Rust - Functional Programming
Rust 맛보기
- 1. Rust - 입문하기
- 2. Rust - 타입 모음
- 3. Rust - Memory Ownership
- 4. Rust - Control Flow
- 5. Rust - Structured Data Types
- 6. Rust - Project Organization
- 7. Rust - Error Handling
- 8. Rust - Collections
- 9. Rust - Generics
- 10. Rust - Test Automation
- 11. Rust - Functional Programming
- 12. Rust - Memory Management
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는 FnMut
와 Fn
를 구현하지 않을 수도 있습니다. 또한 일반적인 함수도 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를 사용한 코드가 더 빠르게 동작한다는 것을 알 수 있습니다.
참고 문헌
-
고려대학교 컴퓨터학과 오상은 교수님의 시스템 프로그래밍(COSE322) 과목 강의자료
Rust 맛보기