타입 추론 - 함수 서명과 몸체의 타입 매칭

Posted on April 15, 2021

타입 추론은 하스켈이 가진 굉장히 멋진 특징인데, 이게 또 익숙해지기 전까지는 머리를 아프게 합니다. 하스켈 공부 초반에는 타입 추론과의 전쟁인 것 같습니다. 나중에는 친구가 될지 어쩔지 모르지만, 저는 아직까지는 전쟁 중인 것 같습니다.

다른 함수와 만나기 전에

아래 코드를 보면 GHC가 알아서 추론할 수 있을 것처럼 보입니다.

func :: a -> a
func x = x + 1
-- (+)는 인스턴스가 여러가지 있습니다.

intFunc :: Int -> Int
intFunc x = x + 2

-- func와 intFunc가 "만난다면" 추론이 될 것 같지 않나요?
main :: IO ()
main = do
    print $ func ( intFunc 1)

funcintFunc 결과값 Int 타입이 넘어가니, a타입은 GHC가 Int로 추론할 것 같은데, 에러가 납니다.

No instance for (Num a) arising from a use of+
 ... 
2 | func x = x + 1

나중에 다른 함수와 만나서 추론될 때, Num 클래스의 메소드 (+)를 가진 타입으로 추론될지 장담할 수 없습니다. 항상 적당한 함수와 꼭 만난다고 가정하면 안됩니다. 일단은 함수 혼자서 원하지 않는 타입이 들어 오는 걸 막을 수 있어야 합니다. func함수는 (+)를 쓰고 있으니, 반드시 Num 클래스만 들어오게 하는 안전 장치가 있어야 합니다. 타입 추론할 때, 다른 함수와 만나는 접점으로 타입을 추론하는 것보다 먼저 각각의 함수들이 서명과 몸체의 타입이 일치하는지 봅니다. 정리하면,

  1. 각각의 함수마다 서명과 몸체의 타입이 일치하는지 보고
  2. 그 다음 다른 함수와 매칭을 살핍니다.

※ 에러의 구체적인 뜻 참고 - 컴파일 에러 읽기 포스트

클래스는 미리 알려줄게, 구체적인 코드는 나중에 결정해

func :: Num a => a -> a
...

Num이라는 클래스를 알려주면 GHC는 불만 없이 다음으로 넘어갑니다. 나중에 조립할 때 GHC가 Num 인스턴스가 넘어오는지 살펴 보기로 약속하고 넘어 가는 겁니다. 저는 이 걸 구체 코드 결정을 “뒤로 미룬다”고 표현합니다. 반드시 특정 타입에서 돌아가는 코드가 아닌 특정 종류class의 타입에서 돌아가는 코드라면 constraint를 통해 종류를 알려주고, 언젠가 다른 함수나 컨텍스트를 만나면 구체 타입이 결정되게 할 때 constraint를 씁니다.

항상 완벽한 구체 타입을 알아야 하는 건 아니다

func :: [a] -> Int
func xs = length xs

main :: IO ()
main = do
    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
isJust Nothing = False
isJust _       = True

Maybe a에서 a타입은 알 필요가 없습니다. 가장 바깥만 알면 됩니다. 항상 완벽한 구체 타입을 알아야 하는게 아니라, 필요한 만큼만 알면 됩니다.

여기 내용과 꼭 관련된 건 아니지만, 가끔은 완전히 구체 타입까지 알아내서 다르게 동작해야 할 필요가 있습니다. 그럴 때 FlexibleInstances 확장을 씁니다. ※ 참고 - 확장 FlexibleInstances 포스트

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com