컴파일 에러 읽기 - rigid type variable

Posted on July 15, 2020

rigid 단어 뜻 : 엄격한, 융통성이 없는

아무 타입도 타입 지정이다!

rigid 단어를 보면 타입이, 다른 타입으로 추론할 수 없게 어느 하나로 고정됐다는 의미처럼 보입니다. 고정된 건 맞는데, 어느 타입 하나로 고정된 게 아닙니다. “여러 타입이 될 수도 있는 성격”으로 함수 서명에서 고정됐다는 의미입니다. 다른 말로 사용자가 지정한 타입(user specified type - 추론 가능한 열린 타입이 아닌, 닫힌 타입으로 사용자가 지정했다라는 의미 같습니다.)이라 말 하기도 합니다.

f :: a -> Bool
f a = a == "x"

a는 아무 타입이나 된다고 했으니, String 타입으로 추론하면 아무 문제가 없을 것 같지만 에러가 납니다. (보통 폴리모픽 변수를 쓰면 forall이 생략되었다고 보면 됩니다.)

Couldn't match expected type ‘a’ with actual type ‘[Char]’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          f :: forall a. a -> Bool
    ...

a는 타입이 정해지지도 않았는데, [Char]와 매치할 수 없다는 에러입니다.
a는 아무타입이나 될 수 있으니 추론하면 되는 거 아니었어?”
forall a를 오해해서 생기는 혼란입니다. a는 정해지지 않은게 아니라, “모든 타입”으로 정해졌습니다. a는 모든 타입을 받는다고 해 놓고, 함수 내부에서는 "x"와 같은 String 타입이어야 한다고 해놨으니 GHC가 매치가 안된다고 불평합니다.

이미 서명에서 지정된 타입

폴리모픽 타입 변수 forall a. a의 해석이 가끔 헷갈립니다.

  1. “나는 아무 타입이나 받아도 내 작업을 할 수 있어”
  2. “GHC가 알아서 적당한 타입으로 바꿔 줄거야”

2번으로 해석하면 위와 같은 오류가 안나고 aString으로 추론해줘야 할 것 처럼 보입니다. 하지만 선언에서 모든 타입이라고 선언해 놨으니 함수 내부에서 String이라는 단일 타입으로 고정하면 안됩니다. 1번으로 해석해야 혼란이 덜 생깁니다.

정리하면, 위 함수는 선언은 모든 타입, 함수 내부에서는 String 단일 타입으로 쓰고 있기 때문에, 선언과 정의가 맞지 않아 에러가 납니다.

타입이 한정된 경우

Eq a => a -> [a] -> Bool
elem _ []       = False
elem x (y:ys)   = x == y || elem x ys

함수 서명에서 aEq a 인스턴스가 있는 타입으로 한정됐습니다.
함수 내부에서도 == 을 만나면 aEq a 인스턴스가 있는 타입으로 한정됩니다.
함수 서명과 함수 내부가 매치됩니다. 타입 추론 작업이 타입 서명을 벗어나지 않습니다.

함수에서 constraint의 의미는, constraint를 추론 단서로 쓰는게 아니라,
“함수 body에서 (==)을 쓰는 걸 봐서, Eq 클래스의 인스턴스만 받는다고 constraint를 이용해 외부에 알려줘” 입니다.

필요한 만큼만 추론

length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs

많은 문서에서 예시로 나오는데, elem과 마찬가지로 length 경우도 a의 구체 타입은 알 필요가 없습니다.

타입 추론은 항상 구체 타입이 결정될 때까지 하는 게 아닙니다. 소속 타입 클래스까지만 추론해서 작업을 할 수 있으면 클래스까지만 추론합니다. elem에서는 (==) 메소드를 쓰니 Eq 클래스의 인스턴스까지만 확인합니다.

head :: [a] -> a
head (x:_) = x

이 함수만의 작업에선 a가 무슨 타입인지 알 필요가 없습니다. 이 함수가 다른 코드와 조합할 때 a의 구체 타입이 필요해지면 그 때 가서 더 추론하면 됩니다.

> :t head
head :: [a] -> a -- a는 아직 추론될 필요가 없습니다.
> :t ("p"++).head
("p"++).head :: [[Char]] -> [Char] -- ("p"++) 함수 때문에 Char로 추론되었습니다.
Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com