Rust에서 count_digits 함수 구현
Clojure와 같은 동적 언어에서는 숫자 타입을 신경쓰지 않고 간단한 계산 함수를 구현할 수 있는 점이 편리하다. 예를 들어 정수 자릿수를 구하는 함수는 다음과 같이 간단히 구현할 수 있다.
(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;
}
}
T
에 Int
뿐만 아니라 PartialOrd
, DivAssign
, Copy
가 추가된 것을 볼 수 있다. PartialOrd
는 비교(>=
)를 위해, DivAssign
은 나눈 뒤 대입(/=
)을 위해 필요하다. 또한 대입(=
, /=
) 또는 비교(>=
) 시 소유권 이전(move) 또는 빌림(borrow)이 발생하는데, 정수 데이터 타입은 모두 Copy
타입이므로, T
에 Copy
트레잇을 추가했다.
다음과 같이 간단히 테스트를 만들어 돌려보면 잘 동작하는 것을 확인할 수 있다.
#[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
를 사용하려면 외부 라이브러리를 사용해야 하며, BigInt
는 Copy
타입이 아니므로 코드를 조금 수정해야 한다.
num 크레잇에는 BigInt
뿐 아니라 위에서 정의한 Int
트레잇과 비슷한 Num
트레잇이 있다. num
크레잇을 사용한 count_digits
구현은 여기를 참조할 수 있다.
count_digits
구현에서 빠뜨린 것이 하나 더 있다. 바로 음수 처리다. count_digits
함수에 음수를 넣으면 항상 1을 리턴한다. 언듯 생각하면 쉽게 해결할 수 있을 것 같다. 음수가 주어지면 양수로 바꿔서 계산하면 되기 때문이다. 그런데 양수로 바꾸는 부분에서 문제가 생긴다.
함수 파라미터 타입이 T
인 것에 주의해야 한다. 예를 들어 n
이 음수인 경우 n = -n
으로 양수로 바꿀 수 있을 것 같다. -n
과 같이 쓰려면 T
는 Neg
트레잇을 구현해야 하는데, 그러면 u8
, u16
과 같은 부호 없는 정수 타입을 사용할 수 없게 된다. n.abs()
를 이용해 계산 전에 항상 양수로 바꿔주는 방법을 고려할 수도 있지만, T
에는 abs
메서드가 없다.
음수를 처리하려면 다른 방법을 생각해야 한다. 기본 알고리즘은 어차피 주어진 숫자를 10으로 몇 번 나눌 수 있는지 확인하는 것이다. 함수 코드를 조금 바꾸면 음수에서도 동작하도록 수정할 수 있다. 문제가 되는 부분은 10과 비교하는 부분이다. 함수 인자로 음수가 주어지면 항상 10보다 작기 때문에 바로 리턴해버리는 것이 문제다.
loop {
if n < ten { // true when n is negative
return count;
}
...
따라서 if n < ten
을 if 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을 리턴하도록 했다. 테스트에 음수를 추가해보면 잘 동작하는 것을 확인할 수 있다.