DataScience
Published 2023. 4. 19. 10:57
Rust 함수 Rust
728x90

함수 선언하기

함수의 입력으로 정수 두 개를 받은 다음 두 수의 합을 리턴하는 add 라는 함수를 만들어 보겠습니다.

먼저 파이썬 코드는 다음과 같습니다. 여기서 타입 힌트를 사용해 파라미터와 리턴값의 타입을 명시할 수 있습니다. 파라미터 변수 이름 뒤에 :int를 붙여 이 파라미터의 타입이 int임을 명시합니다. 함수에서 리턴하는 값은 함수명 뒤에 -> int와 같이 표기합니다.

def add(num1: int, num2: int) -> int:
    return num1 + num2

동일한 기능의 러스트 코드는 다음과 같습니다. 함수의 선언에 fn 키워드를 사용하고, 함수에서 실행할 코드를 중괄호로 묶어줍니다. 그리고 파이썬과 비슷하게 파라미터에는 :i32로 타입을 표기하고, 리턴값에는 -> i32처럼 화살표를 사용해 타입을 명시했습니다.

fn add(num1: i32, num2: i32) -> i32 {
    return num1 + num2;
}

이때 주의해야 하는 점은 파이썬에서는 타입을 생략할 수 있지만, 러스트에서는 반드시 파라미터와 리턴 타입을 명시해야 한다는 것입니다. 타입이 잘못되거나 표기되지 않았다면 컴파일되지 않습니다.

 

러스트는 코드 마지막에서 return 키워드를 생략할 수 있습니다. 이때 세미콜론이 없다는 점에 주의하세요. 다음 코드는 위에서 정의한 add 와 완전히 동일합니다.

fn add(num1: i32, num2: i32) -> i32 {
    num1 + num2
}

add 함수를 메인 함수에서 호출하고 값을 프린트해 보겠습니다.

fn add(num1: i32, num2: i32) -> i32 {
    num1 + num2
}

fn main() {
    println!("{}", add(1, 2));
}

실행 결과

3

함수의 호출은 파이썬과 동일하게 함수명(파라미터, 파라미터, ...) 와 같이 할 수 있습니다.

 

여러 개의 값 리턴하기

이번에는 함수에서 여러 개의 값을 리턴하는 경우를 살펴보겠습니다. 입력받은 두 정수를 순서를 바꿔서 리턴하는 함수를 만들어 보겠습니다. 먼저 파이썬에서 swap 이라는 함수를 아래와 같이 구현합니다. 이렇게 여러 개의 값을 리턴하는 경우, 리턴 타입이 튜플이 됩니다.

def swap(num1: int, num2: int) -> tuple[int, int]:
    return num2, num1


num1, num2 = swap(1, 2)
print(f"{num1}, {num2}")

실행 결과

2, 1

 

이번에는 러스트 코드입니다. 러스트도 여러 개의 값을 리턴하는 경우, 값들이 튜플로 묶이게 됩니다. 따라서 리턴하는 두 정수를 소괄호로 묶어서 (num2, num1) 과 같이 튜플임을 표시합니다. 따라서 함수의 리턴 타입도 튜플로 (i32, i32) 표기합니다.

fn swap(num1: i32, num2: i32) -> (i32, i32) {
    (num2, num1)
}

fn main() {
    let (num1, num2) = swap(1, 2);
    println!("{num1}, {num2}");
}

실행 결과

2, 1

만일 main 함수와 같이, 함수에서 리턴하는 값이 없는 경우에는 리턴 타입을 생략하거나 ()와 같이 아무 것도 리턴하지 않음을 표기할 수 있습니다. 파이썬에서 아무것도 리턴하지 않는 경우, -> None 으로 표기하거나 표기를 생략하는 것과 비슷합니다.

fn do_nothing() -> () {
    return ();
}

fn me_too() {}

fn main() {
    println!("{:?}", do_nothing());
    println!("{:?}", me_too());
}

 

스코프

스코프(scope)란 변수에 접근할 수 있는 범위를 의미합니다. 먼저 파이썬에서는 스코프를 기본적으로 함수 단위로 구분합니다.

실제로는 파이썬은 LEGB 룰이라고 불리는 좀더 복잡한 스코프 규칙을 가지고 있지만, 여기서는 단순화해서 함수 기준으로 설명합니다.

 
def hello(name: str):
    num = 3
    print(f"Hello {name}")


if __name__ == '__main__':
    my_name = "buzzi"

    if True:
        print("My name is", my_name)
        my_name = "mellon"

    hello(my_name)

    # print(num) # error

실행 결과

My name is buzzi
Hello mellon

코드 실행 부분을 먼저 보면, my_name 변수에 "buzzi" 라는 문자열을 할당합니다. 그 다음 if 문에서 변수 값을 프린트해보면 "buzzi"가 프린트됩니다. 하지만 그 다음 라인에서 my_name = "mellon" 으로 변수의 값을 바꿔 버렸습니다. 파이썬은 스코프를 함수 단위로만 구분하고 있기 때문에 이제 코드 전체에서 값이 바뀌게 됩니다. 따라서 hello(my_name)의 출력은 Hello mellon이 됩니다. 마지막으로 # print(num) # error를 주석 해제하고 실행해 보면 에러가 발생합니다. hello 함수 안에서 선언된 num 이라는 변수를 프린트하기 때문입니다. 즉, num 의 스코프가 hello 함수이기 때문에 함수 바깥에서 참조할 수 없는 것입니다.

 

이번에는 러스트의 스코프를 살펴보겠습니다.

fn hello(name: String) {
    let num = 3;
    println!("Hello {}", name);
}

fn main() {
    let my_name = "buzzi".to_string();

    {
        println!("My name is {}", my_name);
        let my_name = "mellon";
    }

    hello(my_name);

    // println!("{}", num); // error
}

실행 결과

My name is buzzi
Hello buzzi

러스트에서는 스코프를 중괄호 "{}" 기준으로 구분합니다. 먼저 my_name 변수를 "buzzi"로 할당했습니다. 그 다음, 중괄호 안에서 my_name 을 프린트해보면 "buzzi"가 프린트됩니다. 중괄호 안에서 my_name 을 "mellon"으로 할당하더라도, 중괄호를 벗어나면 중괄호 안에서 선언된 my_name 의 스코프가 끝나게 되므로 중괄호 바깥에서는 my_name 의 값은 원래대로 "buzzi"가 됩니다. 따라서 hello(my_name)의 실행 결과는 "Hello buzzi"가 됩니다. 파이썬에서와 마찬가지로, hello 안에서 선언된 변수인 num은 함수 바깥에서 참조할 수 없기 때문에 println!("{}", num); 을 주석 해제한 다음 코드를 실행하면 에러가 발생합니다.

러스트의 스코프는 소유권 모델과 밀접한 연관이 있기 때문에 중괄호를 기준으로 스코프가 변경된다는 사실을 꼭 기억해 두세요.

 

익명 함수

익명 함수란 이름이 없는 함수라는 뜻으로, 프로그램 내에서 변수에 할당하거나 다른 함수에 파라미터로 전달되는 함수입니다. 따라서 익명 함수를 먼저 만들어 놓고 나중에 함수를 실행할 수 있습니다.

파이썬에서는 익명 함수를 람다 함수(Lambda function)이라고 부릅니다. lambda 키워드를 쓰고, 파라미터: 리턴값 형식으로 함수의 내용을 정의합니다. 이렇게 만든 람다 함수를 변수 my_func 에 할당해 두었다가 print 함수 안에서 호출하는 예제입니다.

my_func = lambda x: x + 1
print(my_func(3))

실행 결과

4

위 예제처럼 람다 함수는 다른 함수에 파라미터로 전달하는 것이 가능합니다. 러스트에도 람다 함수와 비슷한 개념이 있는데 바로 클로저(Closure)입니다. 위에서 만든 람다 함수와 동일한 기능을 하는 클로저를 만들어 보겠습니다. 클로저는 파라미터를 | | 의 사이에 선언하고, 그 뒤에 함수에서 리턴하는 부분을 작성합니다.

fn main() {
    let my_func = |x| x + 1;
    println!("{}", my_func(3));
}

실행 결과

4

이때 컴파일러가 클로저의 파라미터와 리턴값의 타입을 i32로 추측해서 보여줍니다. 이는 실제 함수가 실행되는 부분인 my_func(3)로부터 변수 x의 타입을 알 수 있기 때문입니다. 이처럼 클로저는 함수와 다르게 타입을 명시할 필요가 없이 컴파일러가 타입을 추론하도록 할 수 있습니다. 하지만 타입을 명시하는 것도 가능합니다.

fn main() {
    let my_func = |x: i32| -> i32 { x + 1 };
    println!("{}", my_func(3));
}

타입을 명시해야 하는 경우, 함수 실행 부분을 중괄호로 묶어 주어야 합니다.

람다 함수는 반드시 한 줄로만 작성해야 하지만, 클로저는 중괄호로 묶어주는 경우 여러 줄을 작성할 수 있습니다. 위 코드를 하나의 클로저로 바꿔 보겠습니다. 이때 입력받은 변수 x의 값을 바꾸기 위해 가변으로 선언하고, 첫 번째 줄에서 x에 1을 더해줍니다. 그 다음 x를 프린트합니다. 이제 my_func를 호출하면 동일하게 4가 출력됩니다.

fn main() {
    let my_func = |mut x: i32| {
        x = x + 1;
        println!("{}", x);
    };

    my_func(3);
}

참고로 파이썬과는 다르게 클로저의 재귀 호출은 아직 지원되지 않습니다. 파이썬에서 클로저를 이용해 피보나치 수를 계산하는 예제는 다음과 같습니다.

def fibonacci(n):
    cache = {}

    def fib(n):
        if n in cache:
            return cache[n]
        if n < 2:
            return n
        cache[n] = fib(n - 1) + fib(n - 2)
        return cache[n]

    return fib(n)


fibonacci(10)

동일한 로직을 구현한 러스트 코드는 클로저가 자기 자신을 부를 수 없기 때문에 컴파일되지 않습니다.

fn fib(n: u32) -> u32 {
    let cache = vec![0, 1];
    let _fib = |n| {
        if n < cache.len() {
            cache[n]
        } else {
            let result = _fib(n - 1) + _fib(n - 2);
            cache.push(result);
            result
        }
    };
    _fib(n)
}

fn main() {
    println!("{}", fib(10));
}

'Rust' 카테고리의 다른 글

Rust 소유권  (89) 2023.04.21
Rust 반복문  (83) 2023.04.20
Rust 변수  (94) 2023.04.18
개발자들이 가장 사랑하는 언어 Rust 소개  (158) 2023.04.17
Tauri 설치부터 배포까지 (리눅스 빌드)  (96) 2023.04.16
profile

DataScience

@Ninestar

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!