Ferris the crab

Ferris the crab

  Rust 언어는 2015년 1.0 버전이 출시된 이래로 Stable, Beta, Nightly 세가지 채널로 나누어 꾸준히 업데이트 중입니다. 미리 새로운 기능을 써보고 싶은 개발자들은 Beta와 Nightly를 써볼 수 있지만, 라이브러리를 개발하여 배포하거나, 실제 제품을 만들어야 할 때에는 Stable을 선택할 수 밖에 없습니다. 따라서 Stable 채널의 업데이트는 Rust 환경 전반의 업데이트와 같다고 할 수 있고, 새로운 버전이 출시될때마다 Rust 개발자들의 이목이 집중됩니다.

업데이트에는 사소한 버그 수정도 있고, Beta와 Nightly에서 사용되었던 기능들의 안정화도 포함될 수 있으며 꼭 언어와 직접적으로 관련이 없더라도 빌드에 사용되는 도구들의 업데이트도 포함될 수 있습니다. 어떤 업데이트는 넓고 잔잔한 호수에 작은 돌멩이 하나를 던진 것과 같은 작은 파문과 같다면, 어떤 업데이트는 사람들이 오랫동안 기다리던 커다란 댐의 수문 개방과 같은 시원함을 주기도 합니다. 그리고 이번 2022년 6월 30일에 발표된 1.62.0 업데이트는 명백히 후자였습니다.


1. cargo add

  Rust 1.62.0 업데이트에서 가장 주목받은 기능은 단연코 cargo add 였습니다. 원래는 Cargo.toml에서 항상 직접 crate들을 손으로 추가해줬어야 하지만, 이제 Terminal에서 cargo add CRATE_NAME을 입력하면 crate들이 자동으로 추가가 됩니다. 이를 직접 해보기 위하여 다음과 같이 프로젝트를 하나 만들어봅시다.

crate 만들기

crate 만들기

이렇게 하면 rust_1_62_0이라는 폴더가 생성되고 그 안에 src/main.rs 파일과 Cargo.toml이 생성됩니다. 이제 폴더에 들어가서 Cargo.toml 파일을 열어봅시다.

Cargo.toml

Cargo.toml

아직 [dependencies]에 아무것도 추가가 되지 않은 상태인 것을 볼 수 있습니다. 원래는 다른 crate를 쓰기 위하여 여기에 직접 추가해줘야 했지만, 이제 cargo add 명령어로 간단히 추가할 수 있습니다. 여기서는 Rust 수치계산 라이브러리인 Peroxide를 추가해보겠습니다.

cargo add peroxide

cargo add peroxide

이제 Cargo.toml을 다시 살펴보면 다음과 같이 추가되어 있는 것을 볼 수 있습니다.

Revisit Cargo.toml

Revisit Cargo.toml

이 뿐만 아니라 cargo add는 crate를 불러올 때, 특정 features를 선택하여 불러올 수 있습니다. Peroxide를 추가하되, netcdf 형식 파일들의 I/O를 사용하게 해주는 nc feature를 선택하여 추가해봅시다.

cargo add peroxide –features nc

cargo add peroxide –features nc

이제 다시 Cargo.toml를 보면 다음과 같이 바뀐 것을 볼 수 있습니다.

Revisit Cargo.toml again

Revisit Cargo.toml again


2. [default] enum

  Rust에서 #[derive(...)] 구문은 structtrait을 코드없이 구현하게 해주는 아주 편리한 구문입니다. 예를 들어 이차원 벡터 구조체를 만든다면 다음과 같이 구현할 수 있습니다.

#[derive(Debug, Clone, Copy, Default)]
struct Vec2D {
    x: f64,
    y: f64
}

이렇게 구현하면 Vec2D 구조체는 .clone()::default() method를 갖게 되고, 소유권 전달 없이 복사가 가능해집니다. 이것이 가능한 이유는 xy 둘 모두 이미 Clone, Copy, Default가 구현되어 있는 f64타입이기 때문입니다. 이제 다음 코드를 실행해보면 실제로 default method의 결과를 확인해볼 수 있습니다.

fn main() {
    let p = Vec2D::default();
    println!("{:?}", p);
}
// Output: Vec2D { x: 0.0, y: 0.0 }

그런데 그동안 Rust에서는 Enum 만큼은 #[derive(Default)]를 사용할 수 없었습니다. 따라서 Enum에서 Default를 구현할때에는 항상 다음과 같이 명시적으로 구현해주어야 했습니다.

#[derive(Debug)]
enum Physicist {
    Newton,
    Einstein,
    Heisenberg,
    Feynman,
    Weinberg
}

impl Default for Physicist {
    fn default() -> Self {
        Physicist::Newton
    }
}

고작 5줄 정도 추가에 지나지않지만 매번 이를 손으로 해주는 것이 귀찮은 일임은 분명했습니다. 사람들은 2020년부터 꾸준히 이 기능의 추가를 염원했고, 결국 1.62.0에 와서야 구현이 되었습니다. 이제 1.62.0 버전부터는 아까의 코드를 다음과 같이 줄일 수 있습니다.

#[derive(Debug, Default)]
enum Physicist {
    #[default]
    Newton,
    Einstein,
    Heisenberg,
    Feynman,
    Weinberg
}

3. Total Order for Floating point numbers

  수학에서 Order 개념은 크게 Partial order Total order (Linear order) 두 가지로 나누어집니다. 후자를 따르는 집합은 모든 원소들에 순서가 존재해야 하지만, 전자를 따르는 집합은 어떤 원소들은 순서를 매기지 못하더라도 괜찮습니다. Rust에서 Order 개념은 PartialOrd trait과 Ord trait으로 구현되어 있습니다. Ord trait이 구현되어 있는 타입들은 cmp method를 사용하여 비교할 수 있지만, PartialOrd trait이 구현되어 있는 타입들은 partial_cmp로 비교해야 합니다. 여기까지는 수학적 개념과 Rust에서의 구현이 동등한 것 같지만, 가장 큰 차이가 하나 있습니다. 수학에서는 실수집합이 Total ordered 집합이지만, Rust에서는 부동소수점으로 대표되는 실수들은 Partial ordered 집합이라는 것입니다.

이는 비단 Rust만의 문제는 아닙니다. 부동소수점을 표현하는 기준인 IEEE 754에서 정의한 문제이기 때문입니다. 수학에서 실수는 아무 숫자 2개를 잡으면 무조건 비교가능하지만, 컴퓨터에서 부동소수점 타입에는 단순 비교 불가능한 숫자가 존재합니다. 바로 NaN 입니다. 이러한 점 덕분에 정수에서는 작동하는 코드가 부동소수점에서는 컴파일 에러가 발생하는 상황이 많이 발생하였습니다. 다행히 IEEE 754는 2008년 Revision을 통해 NaN을 포함하여 순서를 매기는 Total order 방법을 제시하였고 그것이 Rust 1.62.0에 포함되게 되었습니다. 이제 Floating point를 비교할 때, total_cmp를 사용하면 NaN까지 포함하여 전부 순서를 매길 수 있습니다. 순서는 다음과 같습니다.

  • negative quiet NaN
  • negative signaling NaN
  • negative infinity
  • negative numbers
  • negative subnormal numbers
  • negative zero
  • positive zero
  • positive subnormal numbers
  • positive numbers
  • positive infinity
  • positive signaling NaN
  • positive quiet NaN.

이를 확인하기 위하여 다음과 같은 코드를 실행해봅시다. (peroxide는 print를 위하여 사용하였습니다.)

use peroxide::fuga::*;

fn main() {
    let mut x = vec![1f64, 0f64, -0f64, f64::NAN, -f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
    x.sort_by(|a, b| a.total_cmp(b));
    x.print();
}
// Output: [NaN, -inf, -0, 0, 1, inf, NaN]

이외에도 여러 업데이트들이 있었지만 위 3가지가 가장 인상깊어서 정리해보았습니다. 다른 업데이트들도 궁금하시다면 다음 링크를 참고하세요.