홈으로 돌아가기

함수형 Rust: 불변성과 반복자

이 기사는 Rust에서 함수형 스타일의 장점을 설명합니다: 가변성 대신 불변성, 루프 대신 선언적 반복자, Result와 Option을 사용한 명시적 오류 처리. 중/시니어 개발자를 위한 코드 예제와 권장 사항이 제공됩니다.

Rust에서 깔끔한 코드: 가변성 없는 함수형 접근
Advertisement 728x90

Rust에서의 함수형 스타일: 불변성, 선언적 코드, 에러 처리

Rust의 함수형 프로그래밍은 명령형 및 객체 지향 스타일의 일반적인 문제점인 가변성, 명령형 루프, 암묵적 에러 처리를 해결합니다. 상태를 변경하는 대신, 불변 변환, 선언적 연산, ResultOption과 같은 명시적 타입을 기반으로 코드를 작성합니다. 이로 인해 프로그램이 예측 가능해지고 오류 발생 가능성이 줄어듭니다.

Rust는 기본적으로 불변성을 지향하여 개발자들이 익숙한 패턴을 재고하도록 장려합니다. 함수형 스타일은 반복자, 함수 합성, 패턴 매칭을 사용하여 깔끔하고 부작용 없는 코드를 작성합니다.

가변성 대신 불변성

가변 변수는 숨겨진 의존성을 만듭니다: 값이 예기치 않게 변경되어 디버깅을 복잡하게 만듭니다. 명령형 코드에서 mut x 변수는 선언과 사용 사이의 어느 시점에서나 변경될 수 있습니다.

Google AdInline article slot
fn my_function(num: i32) -> i32 {
    let mut x = num;
    // ...30줄 더...
    x += 96;
    // ...50줄 더...
    x
}

여기서 원래 값이 손상되고, 잠재적인 중간 변경으로 인해 결과가 예측 불가능해집니다. 불변성은 이를 해결합니다: 기존 값을 수정하는 대신 새로운 값을 생성합니다.

let x = 10;
let new_x = process(x); // x는 변경되지 않음

Rust에서 불변성은 표준입니다. 가변성은 명시적인 mut가 필요하지만, 함수형 스타일은 그 사용을 최소화합니다:

  • 반복자나 map을 사용하여 불변적으로 문제를 해결할 수 있는지 확인합니다.
  • 가변성이 불가피한 경우, 가능한 가장 작은 블록으로 격리합니다.
  • 데이터 전파에는 clone이나 차용을 선호합니다.

이는 경쟁 조건 위험을 줄이고 동시성을 단순화합니다.

Google AdInline article slot

선언적 vs 명령형 코드

명령형 코드는 실행 단계를 설명합니다: 루프, 조건, 변이. 함수형 코드는 원하는 결과를 설명합니다. 0부터 9까지 숫자의 제곱을 얻기 위해 명령형 버전은 벡터와 push가 필요합니다:

let mut squares = Vec::new();
for num in 0..10 {
    squares.push(num * num);
}

선언적 접근 방식은 반복자 체인을 사용합니다:

let squares: Vec<i32> = (0..10)
    .map(|x| x * x)
    .collect();

선언적 스타일의 장점:

Google AdInline article slot
  • 적은 코드: 반복자 체인이 루프를 대체합니다.
  • 불변성: 컬렉션 변이가 없습니다.
  • 합성 가능성: 함수가 쉽게 결합됩니다.
  • 동시성: .par_iter()가 잠금 없이 멀티스레딩을 가능하게 합니다.

Rust의 반복자는 지연 평가됩니다: mapcollectfor_each가 호출될 때까지 실행되지 않아 메모리를 최적화합니다.

명시적 에러 처리

다른 언어의 예외는 에러를 숨깁니다: try-catch는 타입과 컨텍스트를 무시할 수 있게 합니다. Rust에서는 Result<T, E>?가 명시적 처리나 전파를 요구합니다.

unwrap을 사용한 나쁜 예:

old_toaster().unwrap(); // 에러 시 패닉

올바른 접근 방식은 matchmap_err를 사용합니다:

let toast = match old_toaster() {
    Ok(t) => t,
    Err(e) => {
        eprintln!("에러: {}", e);
        std::process::exit(1);
    }
};

처리 팁:

  • 완전한 제어를 위해 match를 사용합니다.
  • 에러 변환을 위해 map_err를 사용합니다.
  • 호출자가 처리할 때만 ?를 사용합니다.
  • 편의를 위해 anyhowthiserror 같은 라이브러리를 사용합니다.

이는 프로덕션에서 패닉을 방지하고 컴파일 타임에 에러 고려를 강제합니다.

암묵적 비어있음 제거

명령형 언어에서는 null이 어디에나 숨어 있어 NullPointerException을 유발합니다. Rust는 Option<T>를 사용합니다: Some(value) 또는 None을 명시적으로 표시합니다.

let maybe_candies: Option<Vec<Candy>> = get_candies();
let candies = match maybe_candies {
    Some(c) => c,
    None => Vec::new(),
};

패턴 매칭은 두 경우 모두 처리되도록 보장합니다. 컴파일러가 None을 간과하지 않습니다.

핵심 요약

  • 기본 불변성은 상태 변경으로 인한 버그를 줄입니다.
  • 반복자와 map이 루프를 대체하여 코드를 선언적으로 만듭니다.
  • Result와 Option이 명시적 에러 및 비어있음 처리를 강제합니다.
  • 함수 합성이 테스트와 리팩토링을 단순화합니다.
  • 함수형 스타일은 Rust 작업의 80%에 적합합니다; 가변성은 IO나 성능 핫스팟에만 사용됩니다.

— Editorial Team

Advertisement 728x90

다음 읽기