하스켈 언어에 있는 인스턴스(주로 레졸루션)에 관한 얘기입니다. 다른 언어의 인스턴스로 오해하시며 읽는 분들이 계셔 남겨 놓습니다.
“인자로 받은 타입에 따라, 다른 작업을 하는 같은 이름의 함수method를 만든다.”
추상적으로 표현하면
“클래스는 성격을 선언해 놓고, 클래스의 성격을 인스턴스로 구현한다.”
객체지향OOP에서의 인스턴스와는 다른 의미입니다. OOP에서는 객체 설계도에 해당하는 클래스 선언을 하고, 이 설계도에 따라 만든 구현체를 인스턴스로 표현하는데, 하스켈에서 클래스는 몇 개 함수의 선언을 묶어놓은 셋set이고, 이 함수 선언들을 특정 데이터 타입에 맞게 구현한 것을 인스턴스로 부릅니다. 이렇게 얘기하니 꼭 다른 의미라고 보지 않아도 될 것 같긴 합니다. 무언가 틀에 해당하는 걸 먼저 정해놓고, 틀에 따라 실제 쓸 물건을 만드는 건 똑같습니다. 대부분의 문서에서 다름을 강조하는데, 클래스라는 단어 뜻에만 기대서 본다면 굳이 달라 보이지 않기도 합니다.
class HasName a where
getName :: a -> String
getPower :: a -> Int
instance HasName Player where
= "Player : " ++ name x
getName x ...
“a type a is an instance of the class HasName”
“a
는 HasName
클래스의 인스턴스”
“Player
는 HasName
의 인스턴스다.”
“HasName Player
인스턴스가 있다.”
“Player
타입을 만나면 getName
메소드를 가져올 HasName
인스턴스가 있다.”
대체로 위와 같은 해석이 많이 쓰이는데, 해석만 봐서는 마치 Player
가 HasName
의 구현체처럼 느껴지지 않나요?
class Convertible a b | a -> b where
convert :: a -> b
union :: a -> b -> a
instance Convertible Int Char where
= ...
convert = ...
union
instance Convertible Char Int where
= ...
convert = ... union
HasName Player
처럼 헤드에 매개 변수가 하나만 있을 때는 “Player
는 HasName
의 인스턴스다”란 해석이 그럭 저럭 넘어갈만 한데, “Int Char
는 Convertible
의 인스턴스다”란 해석은 직관적으로 잘 와닿지 않습니다. 저와 비슷한 느낌이 오는 분들은 다음처럼 해석하면 도움이 됩니다.
인스턴스를 정의하는 건 메소드 셋set을 준비하는 겁니다. Convertible
은 convert
, union
으로 이루어진 메소드 셋set의 이름같은 겁니다. Typeclass는 이 셋set을 가진(성격을 가진) Type들의 부류입니다. 인스턴스 헤드에 쓰여 있는 Char
와 Int
는 여러 메소드 셋 중에 하나를 선택할 때 쓰이는 키key또는 태그tag 같은겁니다. Char
와 Int
는 구현체 자체를 뜻하는게 아니라, 여러 구현체중 하나를 고를 때, 다른 구현체와 구별하기 위한 key로 쓰이게 됩니다. 개념적으로 말을 할 땐, Char
와 Int
의 조합은 Convertible
인스턴스다 또는 인스턴스를 가졌다고 말합니다.
“instance Convertible Int Char
정의는 Convertible
의 새로운 메소드 셋(convert
,union
) 한 벌을 추가하고, 이 메소드 셋의 키는 Int Char
로 지정한다.”
Player는 HasName의 구현체가 아니라, HasName 구현체 중 하나를 골라낼 때 쓰는 키입니다.
Player는 HasName 성질을 가지고 있다.
Player는 HasName 인스턴스다.
아마도 논리적인 설계는 다음과 비슷할 겁니다. (주의: 실제 테이블 모양이 이렇다는 얘기는 아닙니다. 정확한 자료를 찾으면 추가하도록 하겠습니다)
인스턴스로 지정되는 타입은 사실 “타입” 자체는 아무것도 바뀌지 않습니다. 코드 조립을 위해 GHC가 관리하는 타입 테이블은 아무것도 바뀌지 않습니다. GHC가 코드 조립 중 메소드를 맞닥뜨렸을 때 메소드 테이블에서 “타입”을 키로 메소드를 선택합니다. 이 키 역할을 해주는 타입을 인스턴스 헤드에 써줍니다. 아마도 다음과 비슷하게 설계되어 있을 겁니다.
getName 메소드 테이블 추정
Key 구현체
======== ==================
Player Player용 getName
Villain Villain용 getName
MyType MyType용 getName
...
위 소스에서는 getName
메소드가 여러 개 있고, 이 중 하나를 선택하는 키로 쓰이는게 Player
, Villain
타입입니다. 메소드가 인자로 받는 타입에 따라 구현체를 골라냅니다.
instance (Eq a) => Eq (Tree a) where
==) ... (
(Tree a)
타입을 Eq
클래스의 인스턴스로 만드는데, Tree
가 감싸고 있는게 Eq
의 인스턴스여야 합니다. 다시 말해, 아무 (Tree a)
타입을 만났다고 모두 이 인스턴스로 코드 조립을 할 수 있는게 아닙니다. a
타입을 다루는 (==)
가 있을 경우만, 이 인스턴스의 메소드로 해결할 수 있습니다. 최종 코드 조립을 할 때는 구체 타입을 알아야 하는데 a는 무슨 타입일까요?
코드 조립할 때, 항상 구체 타입까지 알아야 하는 건 아닙니다.
여기선 구체 타입까지 고정해 놓지 않았습니다.
구체 타입까지 알 필요는 없고, (==)
의 매개 변수가 Eq a => Tree a
타입이면 모두 이 인스턴스의 (==)
메소드를 선택합니다.
Tree Int
, Tree Float
, … 뭐든 Eq
의 인스턴스가 있는 타입이면 모두 위 인스턴스의 메소드를 선택합니다.
(==)
메소드 테이블 추정
Key 구현체
=================== ==========================
Int Int용 (==)
Float Float용 (==)
Eq a) => Tree a (Eq a) => Tree a용 (==)
(...
그럼 이렇게 키에 “(Eq a)
”란 constraint가 Key
로 쓰일 것 같은데, 그렇지 않습니다. 인스턴스를 고를 때 constraint는 전혀 고려하지 않습니다. 그리고, 사실 Tree
이기만 하면 Tree
용 (==)
를 선택합니다. FlexibleInstances 확장을 켜지 않으면, 가장 바깥쪽 생성자(여기선 Tree
)만 키로 사용하므로 Tree a
에서 a
타입도 인스턴스를 찾는 조건이 아닙니다. 아마도 다음과 같을 겁니다. - 왜 기본 동작을 이렇게 설계했는지는 잘 모르겠습니다. 퍼포먼스 문제가 아니였을까요?
Key 구현체
=================== ==========================
Tree Tree용 (==)
그럼 constraint는 언제 영향을 줄까요? 다음 테스트로 짐작할 수 있습니다.
만일 Eq
인스턴스가 아닌 타입 a
를 가진 Tree
를 인자로 (==)
를 부르면 어떤 에러가 날까요? Eq (Tree a)가 없다는 에러가 날까요? 아니면 Eq a가 없다는 에러가 날까요?
import Data.Tree
data NoEq = NoEq
treeA :: Tree NoEq
= Node NoEq []
treeA
treeB :: Tree NoEq
= Node NoEq []
treeB
> treeA == treeB
<interactive>:21:1: error:
No instance for (Eq NoEq) arising from a use of ‘==’ •
위 에러로 Tree
인스턴스는 찾았지만 (==)
의 constraint인 Eq a =>
를 풀지 못해 발생한 에러임을 알 수 있습니다.
다른 방향에서 접근해서 설명하면, constraint만 다른 인스턴스는 정의할 수 없다는 것과 같은 말입니다.
instance (Eq a) => SomeClass a where ...
instance (Show a) => SomeClass a where ...
error:
Duplicate instance declaration: ...
이렇게 정의할 수 없다는 말입니다.
아래와 같이 정의하면
import Data.Tree
data YesEq = YesEq
instance Eq YesEq where
YesEq == YesEq = True
instance Eq (Tree YesEq) where
Node x [] == Node y [] = x == y
instance.hs:9:10: error:
Illegal instance declaration for ‘Eq (Tree YesEq)’
• All instance types must be of the form (T a1 ... an) (
Tree a
처럼 써야지 Tree YesEq
처럼 구체 타입을 써 줄수 없다는 에러입니다. (어차피 Tree
까지만 키로 쓰일테니 YesEq
까지 구체 타입을 적어서 혼란스럽게 만들면 안된다는 취지의 에러 아닐까 추측합니다.)
FlexibleInstances
확장을 켜면
{-# LANGUAGE FlexibleInstances #-}
...
...
instance.hs:10:10: error:
Overlapping instances for Eq (Tree YesEq)
• of ‘GHC.Classes.$dm/=’
arising from a use Matching instances:
instance Eq a => Eq (Tree a) -- Defined in ‘Data.Tree’
instance Eq (Tree YesEq) -- Defined at instance.hs:10:10
Data.Tree
모듈에 Tree
가 Eq
인스턴스를 가질 때 매칭하는 인스턴스가 이미 정의되어 있기 때문에 중복overlapping이라는 에러입니다. “왜 FlexibleInstances를 켰는데도 Tree YesEq가 중복이라는 에러가 나지?” 라고 오해할 수 있습니다.
FlexibleInstances
확장을 켜줬으니 구체 타입까지 써줘도 될 것 같은데, 이미 Data.Tree
에 정의되어 있는 instance
가 폴리모픽 Tree a로 정의되어 있기 때문에 둘 다 매칭되기 때문에 나는 오류입니다. FlexibleInstances 포스트를 참고하세요.
참고
https://mgsloan.com/posts/inspecting-haskell-instance-resolution/