함수
함수 (Functions)
함수는 Rust 코드에서 아주 흔하게 등장합니다. 이미 언어에서 가장 중요한 함수 중 하나를 본 적이 있을 거예요. 바로 많은 프로그램의 진입점인 main 함수죠. 그리고 새 함수를 선언할 수 있게 해주는 fn 키워드도 봤고요.
Rust 코드는 함수와 변수 이름의 관례적인 스타일로 snake case를 사용합니다. 모든 글자가 소문자이고 단어를 밑줄로 구분하죠. 예를 들어 another_function 같은 식이에요. 함수 정의 예시가 들어 있는 프로그램을 하나 보여드릴게요.
Filename: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Rust에서는 fn 뒤에 함수 이름과 괄호 한 쌍을 입력해 함수를 정의합니다. 중괄호는 컴파일러에게 함수 본문이 어디서 시작하고 끝나는지 알려줍니다.
정의한 함수는 이름 뒤에 괄호 한 쌍을 붙여 호출할 수 있어요. another_function은 프로그램 안에 정의되어 있으므로 main 함수 안에서 호출할 수 있습니다. 여기서 another_function을 소스 코드에서 main 함수 뒤에 정의했다는 점에 주목하세요. 앞에 정의해도 됐어요. Rust는 함수를 어디에 정의하든 상관하지 않습니다. 호출자가 볼 수 있는 스코프 어딘가에만 정의되어 있으면 돼요.
함수를 더 탐구하기 위해 functions라는 새 바이너리 프로젝트를 시작해볼게요. another_function 예시를 src/main.rs에 넣고 실행하세요. 다음 출력이 보일 거예요.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
줄들은 main 함수에 나타난 순서대로 실행됩니다. 먼저 "Hello, world!" 메시지가 출력되고, 그다음 another_function이 호출되면서 그 메시지가 출력되죠.
출처: The Rust Book
본문
매개변수 (Parameters)
함수에 *매개변수(parameter)*를 갖도록 정의할 수 있는데, 매개변수는 함수 시그니처(signature)의 일부인 특별한 변수입니다. 함수에 매개변수가 있으면 그 매개변수에 구체적인 값을 제공할 수 있어요. 엄밀히 말하면 그 구체적인 값은 *인자(argument)*라고 부르지만, 일상적인 대화에서는 parameter와 argument라는 단어를, 함수 정의의 변수든 호출 시 전달하는 구체적인 값이든 아무 의미로나 섞어 쓰는 편입니다.
another_function의 이 버전에서는 매개변수를 추가해볼게요.
Filename: src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
이 프로그램을 실행해보세요. 다음 출력이 나와야 해요.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function의 선언에는 x라는 매개변수 하나가 있습니다. x의 타입은 i32로 지정돼 있어요. another_function에 5를 넘기면 println! 매크로가 형식 문자열에서 x가 들어 있던 중괄호 쌍 자리에 5를 넣습니다.
함수 시그니처에서는 각 매개변수의 타입을 반드시 선언해야 해요. 이는 Rust 설계에서 의도된 결정입니다. 함수 정의에 타입 주석을 요구하면, 컴파일러가 코드의 다른 곳에서 어떤 타입을 의미하는지 알아내기 위해 타입 주석을 거의 요구하지 않게 됩니다. 컴파일러는 또한 함수가 기대하는 타입을 알면 더 유용한 오류 메시지를 줄 수 있어요.
여러 매개변수를 정의할 때는 매개변수 선언을 쉼표로 구분하면 됩니다. 이렇게요.
Filename: src/main.rs
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
이 예시는 print_labeled_measurement라는, 매개변수 두 개를 가진 함수를 만듭니다. 첫 번째 매개변수는 value라는 이름이고 i32 타입이며, 두 번째는 unit_label이고 char 타입이에요. 함수는 value와 unit_label 둘 다를 담은 텍스트를 출력합니다.
이 코드를 실행해볼게요. functions 프로젝트의 src/main.rs 파일에 있던 프로그램을 위 예시로 교체하고 cargo run으로 실행하세요.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
함수를 value 값으로 5, unit_label 값으로 'h'를 넣어 호출했기 때문에 프로그램 출력에 그 값들이 들어 있습니다.
문과 표현식 (Statements and Expressions)
함수 본문은 선택적으로 표현식(expression)으로 끝나는 일련의 문(statement)으로 이뤄져 있어요. 지금까지 다룬 함수에는 끝나는 표현식이 포함돼 있지 않았지만, 문의 일부로 표현식을 본 적은 있습니다. Rust는 표현식 기반 언어(expression-based language)이기 때문에 이 구분을 이해하는 것이 중요해요. 다른 언어에는 이와 같은 구분이 없기 때문에, 문과 표현식이 무엇이고 그 차이가 함수 본문에 어떤 영향을 주는지 살펴볼게요.
- *문(statement)*은 어떤 동작을 수행하고 값을 반환하지 않는 지시문입니다.
- *표현식(expression)*은 결과 값을 평가합니다.
예시를 몇 개 보여드릴게요.
사실 우리는 이미 문과 표현식을 사용했어요. let 키워드로 변수를 만들고 값을 할당하는 것이 문입니다. Listing 3-1에서 let y = 6;이 문이에요.
Filename: src/main.rs
fn main() {
let y = 6;
}
함수 정의도 문입니다. 위의 전체 예시 자체가 하나의 문이에요. (곧 보겠지만, 함수 호출은 문이 아닙니다.)
문은 값을 반환하지 않습니다. 그래서 다음 코드가 하려고 하는 것처럼 let 문을 다른 변수에 할당할 수 없어요. 그러면 오류가 나죠.
Filename: src/main.rs
fn main() {
let x = (let y = 6);
}
이 프로그램을 실행하면 이런 오류가 나옵니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
let y = 6 문은 값을 반환하지 않기 때문에 x가 바인딩할 것이 없습니다. 이는 C나 Ruby 같은 다른 언어와는 다른 점인데, 그 언어들에서는 할당이 할당의 값을 반환합니다. 그 언어들에서는 x = y = 6이라고 쓰면 x와 y 둘 다 값 6을 가지게 되죠. Rust에서는 그렇지 않습니다.
표현식은 값으로 평가되며, Rust에서 작성할 나머지 코드의 대부분을 구성합니다. 5 + 6 같은 수학 연산을 생각해보세요. 이것은 값 11로 평가되는 표현식입니다. 표현식은 문의 일부일 수 있어요. Listing 3-1의 문 let y = 6;에서 6은 값 6으로 평가되는 표현식입니다. 함수 호출은 표현식이에요. 매크로 호출도 표현식이고요. 중괄호로 만든 새 스코프 블록도 표현식입니다. 예를 들어.
Filename: src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
이 표현식:
{
let x = 3;
x + 1
}
은 이 경우 4로 평가되는 블록입니다. 그 값이 let 문의 일부로 y에 바인딩됩니다. 끝에 세미콜론이 없는 x + 1 줄에 주목하세요. 지금까지 본 대부분의 줄과 달라요. 표현식에는 끝나는 세미콜론이 포함되지 않습니다. 표현식 끝에 세미콜론을 추가하면 문으로 바뀌고, 그러면 값을 반환하지 않게 됩니다. 함수의 반환 값과 표현식을 탐구할 때 이 점을 염두에 두세요.
반환 값을 가진 함수
함수는 호출한 코드에 값을 반환할 수 있어요. 반환 값의 이름을 붙이지는 않지만, 화살표(->) 뒤에 그 타입을 선언해야 합니다. Rust에서 함수의 반환 값은 함수 본문 블록의 마지막 표현식의 값과 동의어입니다. return 키워드와 값을 지정해 함수에서 일찍 반환할 수도 있지만, 대부분의 함수는 마지막 표현식을 암묵적으로 반환해요. 값을 반환하는 함수의 예시를 보여드릴게요.
Filename: src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
five 함수에는 함수 호출도, 매크로도, 심지어 let 문도 없습니다. 그냥 숫자 5만 있을 뿐이에요. 이건 Rust에서 완벽히 유효한 함수입니다. 함수의 반환 타입도 -> i32로 지정되어 있지요. 이 코드를 실행해보세요. 출력은 이렇게 보일 거예요.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
five의 5가 함수의 반환 값이며, 그게 반환 타입이 i32인 이유예요. 더 자세히 살펴볼게요. 중요한 부분이 두 가지 있는데, 첫째로 let x = five(); 줄은 함수의 반환 값으로 변수를 초기화하는 모습을 보여줍니다. five 함수가 5를 반환하므로 그 줄은 다음 줄과 같아요.
#![allow(unused)]
fn main() {
let x = 5;
}
둘째, five 함수는 매개변수가 없고 반환 값의 타입을 정의하지만, 함수 본문은 세미콜론 없는 외로운 5입니다. 반환하고자 하는 값의 표현식이기 때문이에요.
다른 예시도 볼게요.
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
이 코드를 실행하면 The value of x is: 6이 출력됩니다. 그런데 x + 1이 들어 있는 줄 끝에 세미콜론을 두면 어떻게 될까요? 그것을 표현식에서 문으로 바꾸게 되죠.
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
이 코드를 컴파일하면 다음과 같은 오류가 발생합니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
mismatched types라는 주요 오류 메시지가 이 코드의 핵심 문제를 드러냅니다. plus_one 함수의 정의는 i32를 반환한다고 말하지만, 문은 값으로 평가되지 않으며, 그것이 단위 타입(unit type)인 ()로 표현됩니다. 따라서 아무것도 반환되지 않아 함수 정의와 모순되므로 오류가 발생해요. 이 출력에서 Rust는 이 문제를 해결하는 데 도움이 될 메시지를 제공합니다. 세미콜론을 제거하라고 제안하는데, 그렇게 하면 오류가 고쳐질 거예요.