서브타입 다형성이 없는 세상

복잡한 정보 구조를 모델링할 때 많은 프로그래머가 직관적으로 상속을 떠올리곤 한다. 객체 지향 프로그래밍에 익숙할수록 특히 그렇다. 그 결과 아래와 같은 상속 구조가 만들어진다.

class Circle
  extends Shape
  extends GraphicalObject
  extends Drawable
  extends ...
  extends Object

is-a 관계로 타입을 쌓아올리면서 상속 체인이 깊어지면 하나의 타입을 이해하기 위해 상위 타입을 살펴봐야 하고, 그 상위 타입, 그 상위 타입을 살펴보며 그 안에서 어떤 것들이 상속되는지 알아야 한다. 어떤 상위 타입에서 메서드를 정의하는지, 어떤 하위 타입에서 메서드를 오버라이드했는지 추적하다보면 트리 전체, 코드베이스 전체를 헤매고 있는 자신을 발견하게 된다.

서브타입 다형성으로 관계를 설계할 때, 상속에 다양한 층위의 의도가 뒤섞여 버리는 것이 문제다.

  1. “B는 A의 명세를 만족한다”
  2. “B는 A의 한 종류이다”
  3. “B는 A의 구현을 물려받는다”

이 모든 의도가 extendsimplements 키워드로 선언된다. 서브타입 다형성이 없는 세상에서는 다른 방식으로 다형성을 구현할 수 있다. 이 세상에서는 명세가 가장 중요하다. 다음 코드에서 ShapeDrawable은 명세다.

class Shape a where
  area :: a -> Double
  perimeter :: a -> Double

class Drawable a where
  draw :: a -> IO ()

명세는 "어떤 타입 aShape이려면 areaperimeter 함수를 가져야 한다"고 정의할 뿐이다. Circle이 명세를 만족한다는 사실을 증명하는 것은 별개다. 때문에 관계가 수직으로 쌓이는 대신, 수평으로 펼쳐진다.

data Circle = Circle Double

instance Shape Circle where
  area (Circle r) = pi * r ^ 2
  perimeter (Circle r) = 2 * pi * r

instance Eq Circle where
  Circle r1 == Circle r2 = r1 == r2

instance Show Circle where
  show (Circle r) = "Circle " ++ show r

instance Drawable Circle where
  draw (Circle r) = putStrLn $ "r=" ++ show r

명세와 증명이 분리되어 있으므로 타입의 확장이 자유롭다. extendsimplements로는 다른 라이브러리에 정의된 클래스에 메서드를 추가하는 것이 불가능하다. 그러나 여기에서는 가능하다. 표준 라이브러리에 정의된 Int를 수정하지 않고도 IntDrawable이 되게 만들 수 있다.

instance Drawable Int where
  draw n = putStrLn (replicate n '*')

관련문서

이 문서를 인용한 문서