ABOUT ME

-

Total
-
  • Rust: Closure Syntax
    컴퓨터/Rust 2022. 12. 27. 00:42
    728x90
    반응형

    Closure란

    짧은 일회성 사용 함수를 생성하는 데 매우 유용하다.

    일반 함수와 마찬가지로 다른 함수에 인수로 전달하거나 변수에 저장하거나 함수에서 반환할 수 있다.

    Rust 언어에서는 Fn, FnMut, FnOnce trait가 적용되어 closure가 만들어진다.

    람다 함수랑 비슷하게 생겼고 아래는 간단한 예시. 이 글에서 다양한 예제와 비교로 정리해 보았다.

    fn main() {
        let plus_one = |x: i32| -> i32 { x + 1 }; // CLOSURE
        let result = plus_one(5);
        println!("The result is {}", result); // 6
    }

     

    때때로 move 키워드랑 같이 사용하는 것을 볼 텐데

    move 키워드는 closure가 단순히 변수를 빌리는 것이 아니라 캡처된 변수의 소유권을 가져간다는 것을 나타낸다.

    즉, closure는 변수가 정의된 원래 범위가 종료된 후에도 변수를 사용할 수 있다


    closure를 함수 인자로 옮길 때 type

    일단 타입부터 보고 가자. Fn(i32) -> i32로 closure 타입을 지정한 것을 볼 수 있다. 

    fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    fn call_closure<F: Fn(i32) -> i32>(closure: F, value: i32) -> i32 {
        closure(value)
    }
    
    fn main() {
        let closure = add_one;
        let result = call_closure(closure, 1);
        println!("Result: {}", result); // Output: "Result: 2"
    }

     

    Fn과 fn은 다른 것임

    Fn: 호출할 수 있는 closure을 지칭함

    fn: function pointer 타입을 지칭함

    fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    fn call_closure(closure: fn(i32) -> i32, value: i32) -> i32 {
        closure(value)
    }
    
    fn main() {
        let closure = add_one;
        let result = call_closure(closure, 1);
        println!("Result: {}", result); // Output: "Result: 2"
    }

     

    FnOnce

    FnMut는 Fn에 mutable 하게 만들면 돼서 비슷하니까 FnOnce trait를 알아본다.

    • "FnOnce는 딱 한 번만 closure를 부를 수 있게 하는 trait"
      • FnOnce 특성을 구현하는 closure가 있는 경우 한 번만 호출할 수 있다는 의미다. 부른 후에, 다시 부를 수 없다.

     

    • "closure를 사용할 함수에 전달하고 함수의 내부 context로 이동할 때 유용함"
      • 즉, closure를 함수에 전달하고 싶을 때 FnOnce를 사용할 수 있으며, 함수가 closure의 소유권(ownership)을 가져와서 내부적으로 사용하기를 원할 때 FnOnce를 사용할 수 있다. 함수가 closure를 반환하지 않으므로 다시 사용할 수 없게 된다.

     

    • "주변 환경에서 변수를 캡처하여 사용할 함수로 이동하기 위해 closure를 pass 하는 경우에 자주 사용됨"
      • 즉, 함수로 전달하는 closure가 한 환경에서 변수(closure 외부에 정의된 변수 등)를 캡처하는 경우 함수가 해당 변수의 소유권을 가져와서 내부적으로 사용한다. 함수 외부에서는 해당 변수를 다시 사용할 수 없다.

     

    아래 코드로 예를 들면 x, y 변수를 캡처하는 closure가 있다.

    이 closure를 call_closure_once 함수로 이동시키고 이 함수가 closure를 불러 x, y를 출력함

    (한 번만 부를 수 있음)

    fn call_closure_once<F>(closure: F)
    where
        F: FnOnce(),
    {
        closure();
        closure(); // ERROR
    }
    
    fn main() {
        let x = 5;
        let y = 7;
        let closure = || {
            println!("x: {}, y: {}", x, y);
        };
        call_closure_once(closure);
    }

     

    여기서 볼 수 있는 게 move가 있으면 FnOnce지만, 없어도 FnOnce일 수 있다

    let x = 5;
    let closure = || x;
    
    // This closure captures x by reference and does not move it.
    let x = 5;
    let closure = move || x;
    
    // This closure captures x by value and moves it into the closure.

     

    FnOnce 예제 - closure 두 번 호출

    메인 함수에서 closure를 부르고 (아직 다른 함수로 옮겨지지 않았음), 함수로 옮겨서 closure를 두 번 부르려 하는 예제

    fn main() {
        let x = 5;
        let y = 10;
    
        let closure = || {
            println!("x + y = {}", x + y);
        };
    
        // We can call the closure here, because it hasn't been moved into the function yet.
        closure();
    
        consume_closure(closure);
    }
    
    // This function takes a closure that implements FnOnce and consumes it.
    fn consume_closure(closure: impl FnOnce()) {
        // We can call the closure here, because it has been moved into the function.
        closure();
    
        // We can't call the closure again, because it has been consumed by the function.
        //closure();  // This line would cause a compile error.
    }

     

     

    FnOnce vs Staic class?

    FnOnce는 한 번만 정확하게 closure를 호출할 수 있는 trait다.

    static 클래스는 프로그램이 시작될 때 생성되고 프로그램의 전체 수명 동안 존재하는 개체 유형이기 때문에 정적 클래스와는 다르다.

    반면, closure는 주변 환경의 변수를 캡처할 수 있는 anonymous function으로 특정 콘텍스트 내에서 생성되어 사용되는 경우가 많다.

    FnOnce 특성은 closure를 소비할 함수에 전달하고 함수의 내부 콘텍스트로 이동하려는 경우 유용하다.

    이는 주변 환경에서 변수를 캡처하여 사용할 함수로 이동하는 closure를 전달하려는 경우에 자주 사용된다.

    closure가 사용되면 함수의 내부 컨텍스트로 이동되었기 때문에 더 이상 closure을 호출할 수 없다.

    이 기능은 closure를 한 번만 호출하면 되고 closure를 소비한 후 다시 사용할 필요가 없는 상황에서 유용하다.

     

    Fn, FnOnce, FnOnce 예제

    이 예제에서는 일반 Fn closure, FnOnce closure (i32 캡처) 그리고 FnOnce closure (struct, vec 캡처)

    3개의 closure를 만들어 보았다. 여러 변수들과 사용할 때를 위한 예제이다.

    struct MyStruct {
        x: i32,
        y: i32,
    }
    
    fn main() {
        // Normal closure
        let closure = |x| x + 1;
        println!("Normal closure: {}", closure(1));
    
        // FnOnce closure that takes an i32
        let once = |x: i32| -> i32 { x + 1 };
        println!("FnOnce closure with i32: {}", once(2));
    
        // FnOnce closure that takes a struct and a Vec
        let once_struct = move |s: MyStruct, v: Vec<i32>| -> (i32, i32) {
            (s.x + v[0], s.y + v[1])
        };
        let s = MyStruct { x: 1, y: 2 };
        println!("FnOnce closure with struct and Vec: {:?}", once_struct(s, vec![3, 4]));
    }
    
    
    // Normal closure: 2
    // FnOnce closure with i32: 3
    // FnOnce closure with struct and Vec: (4, 6)

     

    FnOnce mutability 예제

    FnOnce closure에서 만약 Vector를 캡처해서 같이 넘기고 다시 사용하고 싶을 때의 예제이다.

    이동했으면 다시 못 불러오므로 return 해서 변수에 지정해서 쓸 수 있다.

    struct MyStruct {
        // some fields here
    }
    
    fn call_fn_once<F: FnOnce(MyStruct, Vec<i32>) -> (i32, Vec<i32>)>(f: F) -> (i32, Vec<i32>) {
        let s = MyStruct { /* initialize fields */ };
        let v = vec![1, 2, 3];
        let (x, y) = f(s, v);
        (x, y)
    }
    
    fn main() {
        let (x, mut v) = call_fn_once(|s: MyStruct, mut v: Vec<i32>| -> (i32, Vec<i32>) {
            // modify v here
            v.push(4);
            (1, v)
        });
    
        // v is now a mutable vector that you can modify again
        v.push(5);
        
        println!{"{:?}", v}; // [1, 2, 3, 4, 5]
    }

     

    FnMut 예제

    FnMut는 Fn closure와 크게 다를 게 없다. closure를 계속 부를 수 있고 캡처한 변수를 수정할 수 있다.

    struct Data {
        value: i32,
    }
    
    let mut data = Data { value: 10 };
    
    let mut closure = |x: &mut i32| *x += 1;
    
    closure(&mut data.value); // data.value is now 11
    closure(&mut data.value); // data.value is now 12

     

     

    요약

    Fn/FnMut는 여러 번 불러질 수 있지만 이동된 값에서는 호출이 불가능하다.

    FnOnce는 consume, Fn/FnMut는 READ/RW이다. 표로 정리해보았다.

    Trait Can be called multiple times Can be called on moved values
    Fn Yes No
    FnMut Yes No
    FnOnce No Yes

    Trait 정의 Mutability
    Fn<Args> The closure can be called multiple times and borrows its values Read-only
    FnMut<Args> The closure can be called multiple times and mutates its values Mutable
    FnOnce<Args> The closure can be called only once and consumes its values Consumed

     

    아래 모든 closure 타입이 있는 예제를 만들어 보았다.

    closure1은 Fn, closure2는 FnMut, closure3는 FnOnce이다. 주석 처리했으니 읽어가면서 정리하자.

    let x = 5;
    
    // A closure that captures `x` and borrows it as a read-only value
    let closure1 = || x;
    
    // `closure1` has type `Fn() -> i32`
    assert_eq!(closure1(), 5);
    
    // A closure that captures `x` and mutably borrows it
    let mut closure2 = || { x += 1 };
    
    // `closure2` has type `FnMut() -> ()`
    closure2();
    
    // A closure that captures `x` and moves it into the closure
    let closure3 = move || x;
    
    // `closure3` has type `FnOnce() -> i32`
    assert_eq!(closure3(), 6);
    
    // Since `closure3` has consumed `x`, it is no longer available
    // assert_eq!(x, 6); // This line would cause a compile-time error

     

    번외 - 다른 언어에선?

    lifetime, ownership 개념이 없어서 다르지만 closure를 사용하는 방법이다.

    파이썬

    # Python: lambda function as a closure.
    def adder(x):
        return lambda y: x + y
    
    add_five = adder(5)
    print(add_five(10))  # 15

    C언어

    // C : a function pointer as a closure.
    #include <stdio.h>
    
    int add(int x, int y) {
        return x + y;
    }
    
    int main() {
        int (*add_five)(int) = add(5);
        printf("%d\n", add_five(10));  // 15
        return 0;
    }

    Java

    // Java : a lambda expression as a closure.
    import java.util.function.Function;
    
    public class ClosureExample {
        public static void main(String[] args) {
            Function<Integer, Integer> addFive = x -> x + 5;
            System.out.println(addFive.apply(10));  // 15
        }
    }

     

    파이썬에서 lifetime 대충 따라 하기

    찾아보니까 파이썬에서 weakref를 사용하며 reference counter를 컨트롤할 수 있는 것 같다.

    다른 많은 개체가 참조하는 크기가 큰 개체를 사용할 때나, 더 이상 필요하지 않을 때 해당 참조를 GC가 수집하도록 할 수 있고 이를 통해 메모리 사용량을 줄이고 성능을 향상할 수도 있다.

    # can be useful when you have a large object that is referenced by many other objects
    # and you want to allow those references to be garbage collected when they are no longer needed.
    # This can help reduce memory usage and improve performance in certain situations.
    import weakref
    
    
    class Life:
        ...
    
    
    def foo(x):
        # Create a weak reference to the class object
        y = weakref.ref(x)
        print(y())  # Prints the class object
    
    
    x = Life()
    foo(x)  # Pass the class object to the foo function
    x = None  # Remove the strong reference to the class object
    # print(x())  # None, since the class object has been garbage collected

     

    참고

     

    Closures: Anonymous Functions that Capture Their Environment - The Rust Programming Language

    Rust’s closures are anonymous functions you can save in a variable or pass as arguments to other functions. You can create the closure in one place and then call the closure elsewhere to evaluate it in a different context. Unlike functions, closures can

    doc.rust-lang.org

     

    728x90

    '컴퓨터 > Rust' 카테고리의 다른 글

    Rust: Ref, Arc, Rc, Mutex 문법 정리  (0) 2022.12.29
    Rust: Generic Associated Types (GAT)  (0) 2022.12.19
    Rust: Go와 비슷하게 멀티쓰레딩 짜기  (1) 2022.08.29

    댓글