/ PROGRAMMING, RUST

Rust - Functional Programming

Rust 맛보기

Closures

이름이 없는 함수로, 다른 프로그래밍 언어에서 lambda 함수라고 불리는 그것에 해당한다고 볼 수 있습니다.

Closure Syntax

let plus_one = |x: i32| x + 1;
let a = plus_one(5);

let foo_v3 = |x: i32| {
    let mut result = x;
    result += 1;
    result
};

위와 같이 parameter는 || 안에 선언하며, return expression은 {}로 감싸서 표현이 가능합니다.

주의할 점은 parameter와 리턴 값에 type annotation을 생략 가능하지만, 처음으로 호출되는 지점에서 compile time에 type이 결정되기 때문에 다른 type의 argument를 넘겨 줄 수 없습니다. 그리고 type annotation이 없는데 한 번도 호출되지 않는 closure는 타입 추론이 불가능하므로, 컴파일이 되지 않습니다.

Closure를 활용한 Environment Capture

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
    Green,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

Closure의 가장 큰 특징은 argument로 전달되지 않은 외부 변수도 사용할 수 있다는 것입니다. 이를 Environment Capture라고 합니다.

Closure를 활용해 Environment Capture를 수행하면 high readability(Parameter passing 대비), high safety(프로그래머가 신경써야 하는 부분이 적어짐)를 얻을 수 있습니다.

Clousre가 갖는 Trait

Closure는 값을 다루는 방법에 따라 세 가지 trait를 자동으로 구현합니다.

  • FnOnce: Closure가 값을 소비하는 경우

참고로 모든 Closure는 FnOnce를 구현합니다. 그러나 FnOnce를 구현한 Closure는 FnMutFn를 구현하지 않을 수도 있습니다. 또한 일반적인 함수도 FnOnce를 구현하고 있기 때문에, FnOnce를 구현한 타입을 요구하는 함수에 Closure 대신 일반 함수를 넘겨줄 수도 있습니다.

  • FnMut: Closure가 값을 변경할 수 있는 경우, 그리고 값이 closure 밖으로 나가지 않는 경우

  • Fn: Closure가 값을 변경하지 않고, 값이 closure 밖으로 나가지도 않으며 environment의 값을 capture하지 않는 경우

Iterators

통상적으로 Iterator trait를 구현하는 오브젝트의 iterator는 .iter() 메소드로 구할 수 있습니다. 이러한 메소드는 개발자가 구현하지 않아도 default implementation이 존재합니다. 한편 Iterator Trait를 구현하려면 반드시 .next() 메소드를 구현해야 합니다. 이 메소드는 Option<T>를 반환하는데, Some(T)는 다음 값을 가리키고, None은 더 이상 값이 없음을 의미합니다.

.collect() 메소드는 iterator를 소비하면서 iterator의 모든 값을 수집하여 새로운 Collection(Vec 등)을 만들어 반환합니다.

한편 .into_iter() 메소드는 iter()메소드와 달리 원본 Collection 등의 소유권을 가져가는 메소드입니다. 다음과 같이 새로운 Collection을 만들어 반환하는 함수 등에 사용됩니다.

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

만약 여기서 그냥 iter()메소드를 썼다면 벡터에 대한 immutable reference만 얻어지기 때문에 collect()로 생성되는 벡터는 Vec<&Shoe> 였을 것입니다.

Iterator Adaptors

.map() 메소드와 같은 일부 메소드는 iterator의 소유권을 가져가지 않고 또 다른 iterator를 새로 생성합니다. .map() 메소드는 argument로 closure를 받아 iterator의 각 요소에 closure를 적용한 새로운 iterator를 반환합니다. 이처럼 많은 iterator adaptor(<->consuming adaptor)들이 주로 argument로 closure를 받아 environment capture를 수행합니다.

또 다른 예로 .filter()는 closure를 받아 closure의 반환값이 true인 것만 새로 생성된 iterator의 값으로 포함합니다.

Loops vs. Iterators

Loop로 작성할 수 있는 함수를 대신 iterator가 갖고 있는 메소드를 활용해 Functional Programming Style로 더 빠르고 간결하게 작성 가능합니다. 예를 들어 .filter() 메소드는 for loop에서 요소 하나하나를 검사하는 것보다 훨씬 간결하게 같은 기능을 수행할 수 있죠. 직관적으로는 코드 실행도 더 low-level의 loop으로 구현되어 있을 Zero-cost abstraction의 .filter()메소드가 더 빠를 것이라고 예측도 가능합니다. 실제로 테스트해 보면 iterator를 사용한 코드가 더 빠르게 동작한다는 것을 알 수 있습니다.


참고 문헌

Rust 맛보기