단위타입

타입시스템을 이용해서 값의 단위 맞추기

트위터에서 재밌는 링크를 하나 발견했다.

요는 어떤 값의 타입이 동일하지만, 단위가 다르다거나 하는 등의 이유로 다르게 평가될 수 있는 값을 어떻게 안전하게 관리하는가 하는 문제이다. 예를 들어 반지름과 중심각을 받아서 원호의 길이를 계산하는 함수를 작성한다고 생각해보자.

import Glibc
func getArcLength(radius: Double, angle: Dounle) -> Double {
  return radius * angle
}

let l = getArcLength(radius: 4, angle: M_PI / 4) /// 3.14159265358979

간단한 문제이다. 그런데 이는 생각치 못한 방향으로 흘러갈 수 있다. 왜냐하면 이 함수에서 사용한 공식은 중심각의 크기가 라디안일 때 적용되기 때문이다. 사용자들은 이렇게 쓸지도 모른다.

let l = getArcLength(radius: 4, angle: 45)

이는 원하는 결과가 아니다. 이 문제 대해서 링크한 슬라이드에서는 NSMeasurement나 Phantom Type을 쓰라고 이야기한다. Measurement는 길이나 각도 등의 값을 단위와 함께 저장하여 변환하는 등의 기능을 제공하는 클래스이다. Phantom Type은 그저 타입 구분을 위해서 빈 타입을 정의하고, 이를 다른 타입으로 감싸서 동일한 원시 타입 값을 구분하게 하는 것이다.

struct Degree {}
struct Radian {}
struct Angle<T> {
  let value: Double
}

func getArcLength(radius: Double, angle: Angle<Radian>) -> Double {
  return radius * angle.value
}

func getArcLength(radius: Double, angle: Angle<Degree>) -> Double {
  return radius * angle.value / M_PI * 180
}

이 문서가 소개하지 않은 방법이 하나 있는데 그것은 바로 enum을 이용하는 것이다. 실제로 Swift와 유사한 강타입언어인 하스켈에서는 이런 방식으로 똑같은 숫자값으로 이루어진 값들을 완전히 다른 타입으로 다룰 수 있다.

data Angle = Degree Double | Radian Double deriving (Show)

toRadian :: Angle -> Angle
toRadian (Degree x) = Radian ( x / 180 * pi )
toRadian x = x

getArcLength :: Double -> Angle -> Double
getArcLength r a = let Radian x = toRadian a in r * x

main = print $ getArcLength 4 (Degree 90)
-- 6.283185307179586

같은 방식으로 다음과 같이 작성해보자.

import Glibc

enum Angle {
  case degree(Double)
  case radian(Double)

  func toRadian() -> Double {
    switch self {
      case let .degree(x): return x / 180 * M_PI
      case let .radian(x): return x
    }
  }

  func toDegree() -> Double {
    switch self {
      case let .degree(x): return x
      case let .radian(x): return x * M_PI / 180
    }
  }
}

func getArcLength(radius: Double, angle: Angle) -> Double {
  return radius * angle.toRadian()
}

let l1 = getArcLength(radius: 4, angle: .degree(90)) // 6.28318530717959
let l2 = getArcLength(radius: 4, angle: .radian(M_PI / 2)) //6.28318530717959

라디안 단위이든 도 단위이든 각도라는 카테고리 내의 하위 타입이므로, 이들을 별도의 타입으로 분류하는 것은 enum으로도 충분히 가능한 셈이며, 유령 타입을 쓰는 것보다도 훨씬 더 자연스러운 코드를 만들 수 있다. (유령타입을 쓰는 경우 두 단위 모두를 지원하는 함수를 만드려면 오버로딩해야한다.)