이 글은 사실 realtime님과 댓글 놀이 하다가 쓰게 되었습니다. 저도 요즘 러스트 공부를 하면서 책도 쓰고 개발도 하고 그러므로, 틀린 내용이 있으면 알려주시면 고맙겠습니다.
---------------------------------------------------------------------------------------------------------------------------------
러스트 언어는 c++와 달리 class, new와 같은 키워드가 없습니다. 대신 class처럼 동작하는 것은 다음 struct 키워드로 만듭니다. struct는 C 언어와 비슷하지만 타입을 기입하는 방법(type annotation이라고 합니다)이 변수이름:타입이름 형태로 사용하는 것이 다릅니다.
struct Point {
x: i32,
y: i32,
}
러스트 언어는 new 연산자 없이 다음처럼 구조체 이름을 그냥 쓰고 중괄호 기호로 구조체의 멤버 속성의 이름을 사용하는 방법으로 앞서 만든 Point 객체를 만듭니다.
러스트 언어는 스칼라 언어에서 보는 마크로(macro)기능이 있는데, 스칼라와 달리 마크로는 필수 언어 기능입니다. 마크로는 #[] 형태로 사용하며, 다음 코드는 Debug trait를 Point에 구현하는 내용입니다. (아래 코드형태를 메타프로그래밍이라고 합니다. https://medium.com/@phoomparin/a-beginners-guide-to-rust-macros-5c75594498f1 를 참고해 보세요).
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
구조체가 Debug를 구현하고 있으면 다음처럼 println("{:#?}", pt)형태로 구조체의 구체적인 내용을 출력해 볼 수 있습니다.
구조체에 add와 같은 메서드를 구현하고 싶을 땐 보통 다음처럼 구현하고자 하는 메서드를 멤버로 가지는 Add와 같은 트레이트를 먼저 만듭니다. 여기서 self는 키워드로서 pt.add(Point{x:1, y:1})형태로 add 메서드를 호출하면 &pt가 self가 됩니다. 즉 C++언어의 this에 해당합니다.
trait Add {
fn add(&self, pt: Point) -> Point;
}
그리고 다음처럼 Add란 이름의 trait를 Point 구조체에 impl/for 키워드로 구현합니다. 이 구문은 go언어에서 struct에 메서드를 구현하는 방식과 많이 비슷합니다.
impl Add for Point {
fn add(&self, pt: Point) -> Point {
Point {
x: self.x + pt.x,
y: self.y + pt.y,
}
}
}
이제 다음 화면의 22번 줄에서 보듯 add 메서드를 호출하여 만들어진 pt2 객체의 내용을 볼 수 있습니다.
이제 add hoc polymorphism(임시 다형성으로 번역합니다) 입니다. 임시 다형성은 C++ 언어의 operator overloading과 개념적으로 비슷합니다.(https://en.wikipedia.org/wiki/Operator_overloading )
다음 C++ 코드는 Time이란 클래스에 + 연산자를 구현하고 있습니다.
Time operator+(const Time& lhs, const Time& rhs) {
Time temp = lhs;
temp.seconds += rhs.seconds;
temp.minutes += temp.seconds / 60;
temp.seconds %= 60;
temp.minutes += rhs.minutes;
temp.hours += temp.minutes / 60;
temp.minutes %= 60;
temp.hours += rhs.hours;
return temp;
}
이 덕분에 t1 + t2와 같은 구문이 가능해 집니다. 이와 비슷하게 + 연산자를 아무 클래스에나 구현해 넣으면 해당 클래스 객체들에 + 연산자를 사용할 수 있습니다. 1 + 1 = 2인 이유는 int 타입의 경우의 + 연산자는 수를 더하는 방식으로 구현한 것이고, "1" + "1" = "11"인 이유는 const char* 타입의 경우의 +연산자는 string catenation을 하는 방식으로 구현하면 되는 것 입니다.(이 부분은 정확히는 std::string을 사용해야 겠지만 개념적으로 이렇다는 의미로 읽어주세요. 저도 c++ 안한지 근 12년이 넘어 가물가물 하네요 ^^.)
c++ 언어는 interface 나 trait란 키워드가 없기 때문에 operator overloading 메커니즘을 class에 구현하지만, 반대로 러스트는 class나 interface 키워드가 없으므로 trait란 키워드에 operator overloading을 구현합니다.
사실 러스트 언어는 Add란 이름의 trait가 있습니다. 그리고 이 Add는 앞서본 add란 이름 대신 + 를 사용하게 합니다. 그런데 Add를 사용하려면 Point는 PartialEq 트레이트를 먼저 다음처럼 구현해야 합니다.
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
그러면 Point에 std::ops namespace의 Add 트레이트를 다음처럼 구현할 수 있습니다. 아래 코드에서 'type Output=Point' 부분을 연관타입(associative type)이라고 합니다.
use std::ops::Add;
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
그러면 다음 화면에서 보듯 Point 객체에 + 연산자를 사용할 수 있습니다.
지금까지 내용을 정리하면 + 연산자는 Point의 전유물이 아닙니다. 그 어떤 구조체라도 Add를 'impl Add for 구조체' 형태로 구현해 놓으면 + 연산자를 쓸 수 있습니다. 러스트 컴파일러는 a + b와 같은 구문을 만나면 a 와 b의 타입을 찾아(즉 구조체를 찾아) 해당 타입이 Add를 구현했는지, 그리고 피 연산자의 타입이 적절한지를 파악하여 코드를 컴파일하거나 오류 메시지를 나타냅니다. 즉 'a + b' 형태의 코드는 변수 a와 b의 타입이 무엇인지에 따라 각기 다른 동작을 하게 됩니다. 즉 다형성(polymorphism)이 발생하는 것이지죠. 이것은 c++에서 흔히 보는 클래스를 상속하여 virtual 메서드를 작성하는 방식의 서브타입(혹은 서브타이핑) 다형성과는 좀 다른 형태의 다형성입니다. 하여 이 둘을 구분하기 위해 임시 다형성(ad hoc polymorphism)이란 용어를 사용합니다.
그런데 러스트의 프레임워크를 사용하는 경우, 대부분 'impl 트레이트 for 구조체' 형태로 구현하기 보다는 다음처럼 마크로를 사용하여 구현하게 됩니다. 즉 Queryable이란 트레이트을 구현하는 수고를 덜어주는 거지요.(이 코드는 diesel이란 이름의 orm 패키지를 사용할 때의 코드입니다)
#[derive(Queryable)]
pub struct Post {
pub id: i32,
pub title: String,
}
러스트 언어는 최근 async/.await 기능이 추가되었습니다. 다음 코드는 actix-web의 'hello world' 코드로서(https://github.com/actix/examples/blob/master/hello-world/src/main.rs ), 아래 main 함수는 앞에 async 키워드가 붙어 있습니다. 그리고 이 덕에 코드 맨 아래의 " .bind("127.0.0.1:8080")?.run() .await"의.await 가 동작할 수 있게 됩니다.
use actix_web::{middleware, web, App, HttpRequest, HttpServer};
async fn index(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!"
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger
.wrap(middleware::Logger::default())
.service(web::resource("/index.html").to(|| async { "Hello world!" }))
.service(web::resource("/").to(index))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
러스트 언어로 쓰레드가 아닌 async io 방식의 코드를 작성하려면 futures 와 tokio란 이름의 crate(크레이트로 발음합니다. 다른 언어에서 패키지에 해당합니다)을 참조하시면 됩니다.
class Eq a where
(==), (/=) :: a -> a -> Bool
다음 "Learn You a Haskell for Great Good!"는 하스켈 입문서로 잘 알려진 사이트 입니다. 중간에 갑자기 확 어려워져서 그렇지 앞 부분은 그럭저럭 읽을 만 합니다. ^^.
http://learnyouahaskell.com/
cpp에서 interface 역할을 하고 싶으면 virtual을 사용하면 됩니당.
http://www.secmem.org/blog/2019/02/10/rust-procedural-macros-by-example/