🦀 Rust
[Rust 입문] 10. 함수형 프로그래밍 특성 (반복자, 클로저)
date
Mar 23, 2023
slug
rust-fp
author
status
Public
tags
Rust
독학
summary
Rust의 함수형 프로그래밍 성질에 대한 설명입니다.
type
Post
thumbnail
category
🦀 Rust
updatedAt
Mar 23, 2023 11:26 AM
rust는 함수형 프로그래밍에서도 영감을 받아 부분적으로 함수형 프로그래밍의 특성이 적용된다.
클로저
클로저는 언어 차원에서 1급 객체(함수)를 지원한다면 클로저가 동작된다.
rust의 클로저도 변수에 저장하거나 다른 함수의 인자로 넘길 수 있는 함수(1급 객체)가 둘러싼 환경을 캡쳐하고 있다면 클로저라고 한다.
대부분의 다른 언어에서는 클로저의 조건을 만족해야 클로저라 부르지만 rust에서는 익명 함수 자체를 클로저라고 부르는 것으로 보인다.
클로저의 선언
클로저는 변수에 클로저를 할당하는 형태로 사용하며 파이프 문자를 이용해 선언한다.
fn main() { let add = |a: i32, b: i32| -> i32 {a + b}; let sub = |a: i32, b: i32| a - b; let print = |d| println!("data : {}", d); let a = 10; let b = 20; print(add(a, b)); print(sub(a, b)); }
클로저는 보통 좁은 영역에 대해 사용하여 자동으로 타입 추론이 되므로 타입을 기입할 필요가 없지만 애매모호한 경우나 필요한 경우에 타입을 기입할 수 있다. 중괄호도 실행하고자 하는 코드가 한줄이면 중괄호를 사용할 필요가 없다.
클로저의 환경 캡쳐
rust의 클로저도 클로저를 둘러싸고 있는 환경 (변수 등)을 가지고 사용할 수 있다.
fn main() { let rest = 10; let div = |target| target / rest; println!("result : {}", div(100)); }
인수로 입력되는 클로저나 클로저를 리턴하는 함수
rust에선 클로저나 함수 모두 트레잇 바운드를 사용하여 함수를 리턴하거나 인자로 함수를 넘길 수 있다.
인자로 함수와 클로저를 넘기는 예제
fn main() { let local = 10; let add = |a: i32, b: i32| -> i32 {a + b}; let add_local = |a: i32, b: i32| -> i32 {a + b + local}; println!("result : {}", calc(10, 20, add)); println!("result : {}", calc(10, 20, sub)); println!("result : {}", calc(10, 20, add_local)); } fn calc<F: Fn(i32, i32) -> i32>(a: i32, b: i32, f: F) -> i32 { f(a, b) } fn sub(a: i32, b: i32) -> i32 { a - b }
Fn 이라는 트레잇을 사용해 함수나 클로저를 인자로 넘길 수 있다. 위에서 add_local 클로저처럼 환경을 캡쳐해서 넘기는 것은 클로저로만 가능하다.리턴값으로 함수와 클로저를 넘기는 예제
fn main() { let c = gen_closure(); let f = gen_func(); println!("{}", c()); println!("{}", f()); } fn gen_closure() -> impl Fn() -> String { let closure = || String::from("I'm Closure!"); closure } fn gen_func() -> impl Fn() -> String { func } fn func() -> String { String::from("I'm Function!") }
impl Trait 을 이용해 함수나 클로저를 리턴하게 할 수 있다.클로저의 캡쳐 방식과 트레잇
클로저가 값을 캡쳐하는 방식에 따라 세가지 중 하나의 트레잇으로 표현할 수 있다.
FnOnce: 클로저의 환경에서 캡쳐한 변수들을 “소비" 한다. (소유권을 가져간다.) 그래서 한 번만 호출 가능하다.
Fn: 환경으로부터 값을 불변으로 빌려 온다. (읽기만 함)
FnMut: 환경으로부터 값들을 가변으로 빌려 온다. (변경 가능)
move 키워드
클로저는 값을 빌려올지 소유권을 이전할지 알아서 추론하게 된다. 상황에 따라 강제로 소유권을 이전시키고자 할 때
move 키워드를 사용한다.fn main() { let s = String::from("hello!"); let ns = || println!("{}", s); ns(); println!("{}", s); }
위 코드는 환경
s 변수에 대해 소유권을 가져오지 않는다. 이런 경우 s 에 대해 강제로 소유권을 가져오고자 할 때 move 키워드를 사용한다.fn main() { let s = String::from("hello!"); let ns = move || println!("{}", s); ns(); // println!("{}", s); // err }
반복자 (iterator)
반복자를 지원하는 다른 언어들처럼 rust도 반복자 패턴을 사용할 수 있다.
fn main() { let v = vec![1, 2, 3, 4, 5]; let iter = v.iter(); for i in iter { println!("elem : {}", i); } }
for ... in 루프는 루프 순회마다 반복자의 각 요소를 사용한다. for ... in 루프를 사용하지 않고 반복 순회를 할때는 다음과 같다.
fn main() { let v = vec![1, 2, 3, 4, 5]; let mut iter = v.iter(); loop { match iter.next() { Some(v) => println!("elem : {}", v), None => break, } } }
반복자의 next() 메소드를 호출할 때마다 당시에 가리키고 있던 원소를 Option 열거형으로 감싸서 리턴해 준다. 그리고 반복자 내부에서 가리키고 있는 원소를 다음 원소로 이동시킨다.
만약 가리키고 있는 원소가 없다면 None을 리턴해 준다.
Iterator 트레잇
표준 라이브러리에 정의된 Iterator 트레잇을 구현하면 반복자를 사용할 수 있으며 벡터와 같은 컬렉션은 Iterator 트레잇을 구현해 두었기 때문에 사용할 수 있다.
Iterator 트레잇은 다음과 같이 구현되어 있다.
trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // 기타 메소드들... (기본 구현) }
Iterator 트레잇은 가리키는 자료형에 대한 타입을
Item 이라는 이름으로 정의하는 것을 요구하며 next 라는 메소드도 구현하길 요구한다.next 메서드는 인자로 mutable한 형태로 자기 자신을 인자로 받는데 이는 next 메소드가 반복자의 내부 상태를 변경하기 때문이다.위에 적은
next 메소드 외에 구현하는 항목들이 몇가지 더 있다. 하지만 다른 메소드들은 기본구현이기 때문에 next 메소드만 구현하면 다른 메소드들도 사용할 수 있다.iter(), iter_mut(), into_iter()
소유권과 가변/불변으로 인해 세 가지 유형의 반복자를 리턴할 수 있다.
- iter : 반복자가 가리키는 값들은 불변 참조이다.
- iter_mut : 반복자가 가리키는 값들은 수정할 수 있다.
- into_iter : 반복자가 가리키는 값들의 소유권을 얻는다.
Iterator 트레잇의 메소드들
sum
sum 메소드는 반복자가 가리키는 아이템들의 합계를 리턴한다. 이 메소드는 반복자를 “소비" 하기 때문에 이 메소드를 호출한 반복자는 사용할 수 없다.
fn main() { let v = vec![1, 2, 3, 4, 5]; let total: i32 = v.iter().sum(); println!("len : {}", total); // 15 }
map
map 메소드는 인자로 주어진 함수를 이용해 새로운 반복자를 리턴한다.
fn main() { let v = vec![1, 2, 3, 4, 5]; let iter = v.iter().map(|x| x * x); for i in iter { println!("elem : {}", i); // 1, 4, 9, 16, 25 } }
collect
collect 메소드는 반복자를 컬렉션으로 만들어 준다.
fn main() { let v = vec![1, 2, 3, 4, 5]; let iter = v.iter().map(|x| x * x); let new_v: Vec<i32> = iter.collect(); for i in new_v.iter() { println!("elem : {}", i); // 1, 4, 9, 16, 25 } }
filter
filter 메소드는 새로운 반복자를 리턴한다. 반복자의 아이템들의 조건은 인자로 주어진 함수가 참이면 기존 아이템을 추가하고 거짓이면 제외시킨다.
함수는 인자로 아이템의 참조자를 받으며 bool 타입으로 리턴해야 한다.
인자에 타입을 명시할 때 주의해야 하는데 인자는 참조자를 받으므로
& 를 기입하지 않으면 이중 참조자가 되는 것에 유의해야 한다.fn main() { let v = vec![1, 2, 3, 4, 5]; let only_even_iter = v.iter().filter(|&x| x % 2 == 0); // x는 &i32 타입임 let only_odd_iter = v.iter().filter(|x| **x % 2 != 0); // x는 &&i32 타입이 됨 for i in only_even_iter { println!("elem : {}", i); // 2, 4 } for i in only_odd_iter { println!("elem : {}", i); // 1, 3, 5 } }
Iterator 트레잇 구현
Iterator 트레잇을 구현한다면 필요로 하는 반복자를 직접 만들수도 있다. 아래는 255 이하의 소수를 생성해내는 반복자를 구현한 것이다.
위에서처럼 리턴할 데이터의 타입과 next 함수만 구현하면 Iterator에서 제공하는 다른 메소드들도 사용할 수 있다.
fn main() { let primes = PrimeGenerator::new(); for i in primes { println!("elem : {}", i); } let primes_over_100 = PrimeGenerator::new().filter(|&n| n > 100); for i in primes_over_100 { println!("elem : {}", i); } } struct PrimeGenerator { number: u8, } impl PrimeGenerator { fn new() -> PrimeGenerator { PrimeGenerator { number: 2 } } } impl Iterator for PrimeGenerator { type Item = u8; fn next(&mut self) -> Option<Self::Item> { let is_prime = |num| { for i in 2..num { if num % i == 0 { return false; } if num < i * i { break; } } true }; self.number += 1; for i in self.number..255 { if is_prime(i as u64) { self.number = i; return Some(i); } } None } }