🔧

함수와 메서드 — def가 fn이 되면 뭐가 달라지는가

반환 타입, 세미콜론, 그리고 Ruby와 같은 "마지막 식이 반환값" 규칙

Ruby에서 함수를 정의하면 인자 타입도, 반환 타입도 안 쓴다. Rust는 둘 다 명시해야 한다.

# Ruby
def greet(name)
  "Hello, #{name}!"
end

// Rust
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

-> 뒤에 반환 타입을 쓴다. 반환값이 없으면(Ruby의 nil 반환) -> ()이지만 생략 가능.

세미콜론 = 반환값 여부

Ruby와 Rust 모두 "마지막 표현식이 반환값"이라는 규칙이 있다. 근데 Rust에서는 세미콜론을 붙이면 반환이 아니라 statement가 된다.

fn add(a: i32, b: i32) -> i32 {
    a + b    // 세미콜론 없음 → 반환값
}

fn add_broken(a: i32, b: i32) -> i32 {
    a + b;   // 세미콜론 있음 → statement, ()를 반환 → 컴파일 에러!
}

이거 처음에 진짜 헷갈린다. Ruby에서는 세미콜론이 문장 구분자일 뿐 의미가 없는데, Rust에서는 반환 여부를 결정한다.

early return

Ruby처럼 return 키워드로 일찍 빠져나올 수 있다.

fn check_age(age: u32) -> &'static str {
    if age < 18 {
        return "minor";  // early return에는 세미콜론 필요
    }
    "adult"  // 마지막 표현식은 세미콜론 없이
}

return을 쓸 때는 세미콜론을 붙인다. 마지막 표현식으로 반환할 때는 안 붙인다. 이 패턴이 Rust의 관례.

인자 전달 — 값 vs 참조

Ruby에서는 인자 전달 방식을 신경 쓸 일이 없다. Rust에서는 소유권 문제 때문에 빌려줄지(borrow), 넘겨줄지(move)를 결정해야 한다.

// 빌림 — 원본 유지
fn print_name(name: &str) {
    println!("{}", name);
}

// 소유권 이동 — 호출 후 원본 사용 불가
fn take_name(name: String) {
    println!("{}", name);
}

let name = String::from("sehwa");
print_name(&name);  // 빌려줌, name 여전히 사용 가능
take_name(name);     // 넘겨줌, 이후 name 사용 불가

대부분의 경우 &로 빌려주면 된다. "읽기만 할 건데 소유권까지 줄 필요 없지"라는 판단.

클로저 — 인라인 함수

Ruby의 블록/lambda가 Rust에서는 클로저다. 함수 인자로 넘기는 패턴이 거의 같다.

# Ruby
[1, 2, 3].map { |x| x * 2 }
[1, 2, 3].map(&method(:double))  # 메서드 참조

// Rust
vec![1, 2, 3].iter().map(|x| x * 2).collect::<Vec<_>>();

|x|가 Ruby의 { |x| }에 해당한다. 중괄호 대신 파이프(|)로 인자를 감싼다.

핵심 포인트

1

fn 이름(인자: 타입) -> 반환타입 { } 형태로 정의

2

세미콜론 없으면 반환값, 있으면 statement — 이게 핵심

3

early return에는 return 키워드 + 세미콜론 사용

4

인자는 &로 빌려주거나 소유권을 넘긴다

장점

  • 함수 시그니처만 보고 입출력 타입을 알 수 있다
  • Ruby와 같은 "마지막 식이 반환값" 규칙이라 적응이 빠르다

단점

  • 세미콜론 실수로 컴파일 에러 나는 게 초반에 빈번하다
  • 빌림/소유권 결정이 Ruby에는 없는 개념이라 함수 설계가 어렵다

사용 사례

Ruby의 메서드 체이닝을 Rust 함수 조합으로 변환할 때 콜백 패턴을 클로저로 구현할 때

참고 자료