Rust Pitfall: Using Semicolons (;)
Rust를 처음 시작하는 입장에서 당황스러울 만한 점 중 하나로 세미콜론의 사용을 들 수 있다. 비슷한 시기에 등장한 Swift나 Go 같은 언어에서는 문장 끝에 세미콜론을 쓰지 않는 것과 다르게, Rust는 C나 Java처럼 세미콜론(;)이 필수인 언어에 속한다. Python이나 Ruby 이후에 등장한 언어들이 대체로 세미콜론을 빼는 쪽으로 문법을 정하는 경향이 있었던 걸 생각해보면 조금 특이한 점으로 볼 수 있겠다.
앞서 말한 대로 문장 끝에 세미콜론을 생략할 수 없기 때문에, 다음과 같은 코드는 당연히 컴파일 에러가 난다. (Rust Playpen)
fn main() { let a = 1 let b = 2 println!("{} + {} = {}", a, b, a + b) }
anon:3:5: 3:8 error: expected one of `.`, `;`, or an operator, found `let` anon:3 let b = 2 ^~~
하지만 여기서부터부터가 당황스러운 지점인데, 아래 코드는 문제 없이 컴파일을 통과한다.
fn main() { let a = 1; let b = 2; println!("{} + {} = {}", a, b, a + b) }
마지막 문장에 한해서 세미콜론을 생략할 수 있는 걸까? 코드를 다음처럼 바꿔 보면 더더욱 혼란스러운 결과를 볼 수 있다. (Rust Playpen)
fn main() { let a = 1; let b = 2; println!("{} + {} = {}", a, b, a + b); a + b }
anon:5:5: 5:10 error: mismatched types: expected `()`, found `_` (expected (), found integral variable) [E0308] anon:5 a + b ^~~~~ anon:5:5: 5:10 help: see the detailed explanation for E0308 error: aborting due to previous error
에러 메시지를 잘 보면 이게 세미콜론이 빠졌다는 얘기하곤 조금 다른 것 같다. 타입이 안 맞는다고? ()는 또 뭐지? 결론부터 얘기하면 다음과 같은 규칙들 때문이다.
Rust에서 거의 대부분의 문장은 수식으로 취급된다.
흔히 3항 연산자라고 불리는 a ? b : c 꼴의 조건 문법이 Rust에는 없는데, if나 match와 같은 문장이 그대로 수식으로 간주되기 때문이다. 다시 말해 아래와 같은 코드를 짤 수 있다.
println!("{}", if a % 2 == 0 { "even" } else { "odd" });
세미콜론은 여러 문장을 연결한다.
다른 언어에서는 세미콜론이 단순히 문장을 끝낸다는 의미만을 가지는 반면, Rust에서는 여기에 덧붙여, 세미콜론으로 연결된 문장들의 가장 마지막 문장을 전체 문장의 '결과 값'으로 하는 기능이 있다.1 짧게 예를 들면, a; b는 먼저 a를 실행한 뒤 그 결과를 버리고, b를 실행한 결과와 같은 값이 된다. 그래서 여러 문장을 { ... } 블록으로 묶으면, 이 블록의 마지막 문장의 값이 블록 밖으로 '리턴'되는 효과가 난다.
이걸 응용하면 아래와 같은 코드를 쓰는 것도 가능하다.
let numbers = { let mut vec = vec![]; for i in (0..5) { vec.push(i); } vec };
a;은 a; ()과 같다.
여기서 ()은 일명 unit 타입이라 불리는 것으로, C나 Java의 void 타입과 비슷한 역할을 맡는다.2 또한, 위 예제에서의 main() 함수처럼 리턴 값이 없는 함수는 ()을 리턴하는 것으로 취급된다.
함수의 가장 마지막 문장이 세미콜론으로 끝나지 않는다면, 그 문장은 암묵적으로 함수의 리턴 값이 된다.
따라서 아래와 같은 함수를
fn add(a: i32, b: i32) -> i32 { return a + b; }
이렇게 줄여 쓸 수 있게 된다.
fn add(a: i32, b: i32) -> i32 { a + b }
이제 위의 예제를 다시 되짚어보자. 두 번째 코드에서 println!("{} + {} = {}", a, b, a + b)이 문제 없이 통과됐던 것은 println!(..)의 결과 값이 ()이고, 이것이 main() 함수의 리턴 타입인 ()와 같았기 때문이다. 세 번째 코드가 실패했던 것 또한 가장 마지막 문장인 a + b의 타입이 ()가 아닌 무언가이고3, 이것이 main()의 리턴 타입과 맞지 않아서였다.
얼핏 실수로 삽질하기 딱 좋은 문법이라는 생각이 들지만, 실수를 해도 Rust가 강타입 언어이기 때문에 타입 시스템에 의해 컴파일러가 잡아 줄 거라고 다들 생각하는 것 같다. 대다수의 관용적인 Rust 코드가 이 특성들을 적극적으로 사용하니 Rust를 배우고자 하는 사람이라면 이런 코드들에 익숙해지는 것이 좋겠다.
만약 C에 익숙하다면 C의 콤마 연산자(,)가 이것과 비슷하다는 걸 눈치챘을 것이다. ↩︎
하지만 void 타입과 다른 점이 하나 있는데, C나 Java에서 void 타입의 값은 존재하지 않지만, Rust의 () 타입은 ()이라는 값을 딱 하나 갖는다. 굉장히 쓸모 없어 보이지만 의외로 Option<T>나 Result<T, E> 타입 같은 걸 쓸 때 유용하게 써 먹을 수 있다. 자세한 얘기는 이 포스팅의 주제를 벗어나니 다음 기회에 다루도록 하겠다. ↩︎
왜 "()가 아닌 무언가"라고 표현했냐면, Rust는 타입 추론을 하는 언어이고, Rust의 정수 리터럴은 문맥에 따라서 여러 다른 타입으로 추론될 수 있기 때문이다. 에러 메시지에서 "found integral variable"이라고 뜬 이유이기도 하다. 이에 대해서도 다른 포스팅에서 다루도록 하겠다. ↩︎











