타입 추론은 하스켈이 가진 굉장히 멋진 특징인데, 이게 또 익숙해지기 전까지는 머리를 아프게 합니다. 하스켈 공부 초반에는 타입 추론과의 전쟁인 것 같습니다. 나중에는 친구가 될지 어쩔지 모르지만, 저는 아직까지는 전쟁 중인 것 같습니다.
아래 코드를 보면 GHC가 알아서 추론할 수 있을 것처럼 보입니다.
func :: a -> a
= x + 1
func x -- (+)는 인스턴스가 여러가지 있습니다.
intFunc :: Int -> Int
= x + 2
intFunc x
-- func와 intFunc가 "만난다면" 추론이 될 것 같지 않나요?
main :: IO ()
= do
main print $ func ( intFunc 1)
func
에 intFunc
결과값 Int
타입이 넘어가니, a
타입은 GHC가 Int
로 추론할 것 같은데, 에러가 납니다.
No instance for (Num a) arising from a use of ‘+’
• ...
2 | func x = x + 1
나중에 다른 함수와 만나서 추론될 때, Num
클래스의 메소드 (+)
를 가진 타입으로 추론될지 장담할 수 없습니다. 항상 적당한 함수와 꼭 만난다고 가정하면 안됩니다. 일단은 함수 혼자서 원하지 않는 타입이 들어 오는 걸 막을 수 있어야 합니다. func
함수는 (+)
를 쓰고 있으니, 반드시 Num
클래스만 들어오게 하는 안전 장치가 있어야 합니다. 타입 추론할 때, 다른 함수와 만나는 접점으로 타입을 추론하는 것보다 먼저 각각의 함수들이 서명과 몸체의 타입이 일치하는지 봅니다. 정리하면,
※ 에러의 구체적인 뜻 참고 - 컴파일 에러 읽기 포스트
func :: Num a => a -> a
...
Num
이라는 클래스를 알려주면 GHC는 불만 없이 다음으로 넘어갑니다. 나중에 조립할 때 GHC가 Num
인스턴스가 넘어오는지 살펴 보기로 약속하고 넘어 가는 겁니다. 저는 이 걸 구체 코드 결정을 “뒤로 미룬다”고 표현합니다. 반드시 특정 타입에서 돌아가는 코드가 아닌 특정 종류class의 타입에서 돌아가는 코드라면 constraint를 통해 종류를 알려주고, 언젠가 다른 함수나 컨텍스트를 만나면 구체 타입이 결정되게 할 때 constraint를 씁니다.
func :: [a] -> Int
= length xs
func xs
main :: IO ()
= do
main print $ func [1,2,3]
첫 번째 논리대로 하면, func
정의 단독으로 a
에 대한 어떤 힌트가 있어야 할 것 같습니다. 하지만 에러가 나지 않습니다. 위의 첫 번째 예는 특정 클래스의 메소드 (+)
를 써서 나는 에러였습니다. 여기선 특정 클래스의 메소드가 없습니다. length 함수는 []
까지만 알면 모호하지 않습니다.
※ 참고 - 클래스 제약 포스트
> :t length
length :: Foldable t => t a -> Int
length
함수는 리스트 구조가 됐든, 트리 구조가 됐든… Foldable
클래스의 인스턴스이기만 하면 받아들입니다. 안에 들어 있는 a
타입은 뭔지 모르지만 foldable
이 가능한 []
리스트 구조임은 명시했으니, 필요한 정보는 다 있는 상태입니다.
아래는 또 다른 예시입니다. 가장 바깥 타입 생성자(여기서는 Maybe)만 명시해 주면 모호하지 않습니다.
isJust :: Maybe a -> Bool
Nothing = False
isJust = True isJust _
위 Maybe a
에서 a
타입은 알 필요가 없습니다. 가장 바깥만 알면 됩니다. 항상 완벽한 구체 타입을 알아야 하는게 아니라, 필요한 만큼만 알면 됩니다.
여기 내용과 꼭 관련된 건 아니지만, 가끔은 완전히 구체 타입까지 알아내서 다르게 동작해야 할 필요가 있습니다. 그럴 때 FlexibleInstances
확장을 씁니다. ※ 참고 - 확장 FlexibleInstances 포스트