Rust에서 count_digits 함수 구현

내 이 세상 도처에서 쉴 곳을 찾아보았으나, 마침내 찾아낸, 컴퓨터가 있는 구석방보다 나은 곳은 없더라.

Rust에서 count_digits 함수 구현

Clojure와 같은 동적 언어에서는 숫자 타입을 신경쓰지 않고 간단한 계산 함수를 구현할 수 있는 점이 편리하다. 예를 들어 정수 자릿수를 구하는 함수는 다음과 같이 간단히 구현할 수 있다.

Feature image

(defn count-digits [n]
  (loop [n n acc 1]
    (if (< n 10)
      acc
      (recur (quot n 10) (inc acc)))))

Rust와 같은 강타입 언어에서는 이런 융통성을 누릴 수 없다. 같은 함수를 Rust로는 다음과 같이 작성할 수 있다.

fn count_digits(mut n: u32) -> u32 {
    let mut count = 1;
    loop {
        if n < ten {
            return count;
        }
        n /= ten;
        count += 1;
    }
}

그러나 이 구현은 u32 타입에만 쓸 수 있다. Rust에는 이것 말고도 u8, u16, u64, i8, i16, i32, i64등 더 많은 정수형 데이터 타입이 있다. Rust에서 모든 정수형 타입에 대해 동작하도록 count_digits 함수를 만들려면 어떻게 해야 할까?

한 함수가 여러 타입에 동작하도록 하려면 제너릭을 사용해야 한다.

fn count_digits<T>(mut n: T) -> usize {
    ...
}

count_digits 함수는 정수형 데이터 타입에 대해서만 의미가 있다. 타입 파라미터 T에 Rust의 정수형 타입만 쓸 수 있도록 하려면 어떻게 해야 할까?

Rust에서는 기존 타입에 트레잇을 추가할 수 있다. Int라는 트레잇을 정의한 다음 u8, i16 등의 정수 타입에 Int 트레잇을 구현해 모든 정수 타입에 Int을 추가하고 count_digits 함수를 Int 트레잇을 구현한 모든 타입에 대해 동작하도록 수정할 수 있다.

// define Int trait
trait Int {}

// implement Int trait for each integral type
impl Int for u8 {}
impl Int for u16 {}
...
impl Int for i8 {}

// now count_digits will work with the type that implements Int trait
fn count_digits<T:Int>(mut n: T) -> usize {
    ...
}

각 정수 타입에 Int 트레잇을 구현하는 코드는 매크로로 대체할 수 있다.

macro_rules! impl_int_for {
    ( $( $t:ty ) *) => {
        $( impl Int for $t {} )*
    };
}

이 매크로를 이용하면 다음과 같이 간단히 각 정수 타입이 Int 트레잇을 구현하게 할 수 있다.

impl_int_for!(u8 u16 u32 u64 u128 i8 i16 i32 i64 i128);

이제 count_digits 함수 구현을 살펴볼 차례다. count_digits는 입력 받은 숫자가 10으로 몇 번 나눠지는지를 계산한다. 입력 받은 숫자(타입 T)를 10(타입 T)으로 나눌 방법이 필요하다. 따라서 타입 T에 대한 10을 얻을 수 있도록 다음과 같이 Int 트레잇과 impl_int_for! 매크로를 수정한다.

trait Int {
    fn ten() -> Self;
}

macro_rules! impl_int_for {
    ( $( $t:ty ) *) => {
        $( impl Int for $t {
            fn ten() -> Self { 10 }
        } )*
    };
}

이제 count_digits 함수는 다음과 같이 수정해야 한다.

fn count_digits<T>(mut n: T) -> usize
where T: Int + PartialOrd + DivAssign + Copy
{
    let mut count: usize = 1;
    let ten = T::ten();
    loop {
        if n < ten {
            return count;
        }
        n /= ten;
        count += 1;
    }
}

TInt뿐만 아니라 PartialOrd, DivAssign, Copy가 추가된 것을 볼 수 있다. PartialOrd는 비교(>=)를 위해, DivAssign은 나눈 뒤 대입(/=)을 위해 필요하다. 또한 대입(=, /=) 또는 비교(>=) 시 소유권 이전(move) 또는 빌림(borrow)이 발생하는데, 정수 데이터 타입은 모두 Copy 타입이므로, TCopy 트레잇을 추가했다.

다음과 같이 간단히 테스트를 만들어 돌려보면 잘 동작하는 것을 확인할 수 있다.

#[test]
fn test_1() {
    for i in 0..10 {
        assert_eq!(count_digit(i), 1);
    }
}

#[test]
fn test_2() {
    for i in 10..100u128 {
        assert_eq!(count_digit(i), 2);
    }
}

#[test]
fn test_types() {
    assert_eq!(count_digit(101i8), 3);
    assert_eq!(count_digit(101u8), 3);

    assert_eq!(count_digit(12345u16), 5);
    assert_eq!(count_digit(12345i16), 5);
}

Clojure는 BigInt 타입에 대해서도 함수를 그대로 사용할 수 있지만, Rust 구현은 그렇지 못하다. Rust에서 BigInt를 사용하려면 외부 라이브러리를 사용해야 하며, BigIntCopy 타입이 아니므로 코드를 조금 수정해야 한다. num 크레잇에는 BigInt뿐 아니라 위에서 정의한 Int 트레잇과 비슷한 Num 트레잇이 있다. num 크레잇을 사용한 count_digits 구현은 여기를 참조할 수 있다.

count_digits 구현에서 빠뜨린 것이 하나 더 있다. 바로 음수 처리다. count_digits 함수에 음수를 넣으면 항상 1을 리턴한다. 언듯 생각하면 쉽게 해결할 수 있을 것 같다. 음수가 주어지면 양수로 바꿔서 계산하면 되기 때문이다. 그런데 양수로 바꾸는 부분에서 문제가 생긴다.

함수 파라미터 타입이 T인 것에 주의해야 한다. 예를 들어 n이 음수인 경우 n = -n으로 양수로 바꿀 수 있을 것 같다. -n과 같이 쓰려면 TNeg 트레잇을 구현해야 하는데, 그러면 u8, u16과 같은 부호 없는 정수 타입을 사용할 수 없게 된다. n.abs()를 이용해 계산 전에 항상 양수로 바꿔주는 방법을 고려할 수도 있지만, T에는 abs 메서드가 없다.

음수를 처리하려면 다른 방법을 생각해야 한다. 기본 알고리즘은 어차피 주어진 숫자를 10으로 몇 번 나눌 수 있는지 확인하는 것이다. 함수 코드를 조금 바꾸면 음수에서도 동작하도록 수정할 수 있다. 문제가 되는 부분은 10과 비교하는 부분이다. 함수 인자로 음수가 주어지면 항상 10보다 작기 때문에 바로 리턴해버리는 것이 문제다.

    loop {
        if n < ten { // true when n is negative
            return count;
        }
    ...

따라서 if n < tenif n == 0와 같은 식으로 바꾸면 음수를 지원할 수 있다. T 타입에 대한 0을 얻을 수 있도록 Int 트레잇에 zero() 함수를 추가한다.

trait Int {
    fn ten() -> Self;
    fn zero() -> Self;
}

impl_int_for! 매크로에도 zero() 함수 구현을 추가해야 한다.

macro_rules! impl_int_for {
    ( $( $t:ty ) *) => {
        $( impl Int for $t {
            fn zero() -> Self { 0 }
            fn ten() -> Self { 10 }
        } )*
    };
}

그리고 count_digits 함수를 다음과 같이 수정한다.

fn count_digit<T>(mut n: T) -> usize
where
    T: Int +PartialOrd + DivAssign + Copy,
{
    let zero = T::zero();
    if n == zero {
        return 1;
    }

    let ten = T::ten();
    let mut count: usize = 0;
    loop {
        if n == zero {
            return count;
        }
        n /= ten;
        count += 1;
    }
}

수정된 함수에서는 count를 0으로 초기화하기 때문에, 함수 인자로 0이 입력되면 0을 리턴하는 문제가 있다. 따라서 함수 윗 부분에 if 조건을 추가해 인자가 0인 경우에는 바로 1을 리턴하도록 했다. 테스트에 음수를 추가해보면 잘 동작하는 것을 확인할 수 있다.

참고