컴파일 에러 읽기 - Couldn't match type, Could not deduce, No instance

Posted on June 12, 2020

에러 메시지를 문장 그대로 읽으려면 다음에 익숙해져야 합니다.

rising : 타입을 결정해 가는 과정은, 타입 제약을 쌓아가다 한 가지 타입으로 추론 될 수도 있고, 그냥 타입 제약까지만 결정될 수도 있습니다. 전체 진행 과정을 타입 추론이라고 하지만, 하나의 단서로 인해 나온 조건은 추론deduced됐다 표현하지 않고, rising 됐다 표현합니다. (제 생각은 추론deduced된 경우만 있는게 아니라, 지정specified 했을 경우도 있으니 rising이라 표현하지 않았을까 합니다.)

rigid : rigid는 융통성이 없다는 뜻인데, 여기선 어떤 형태로 지정됐기 때문에 더 이상 다른 형태로 추론하지 못한다는 뜻에서 지정으로 번역했습니다.

forall : 아무 타입이나 와도 된다가 아니라, 반드시 아무 타입이 들어와도 문제가 없어야 한다 입니다. 단어 그대로 어느 한 가지 타입이 아닌 모든 타입입니다. 특정 확장들을 켜지 않으면 서명에 드러나진 않지만, 서명에 아무런 제약없이 써있는 소문자들은 forall a 타입으로 보면 됩니다.

(forall ) constraint : constraint를 써주는 것도 사실은 forall이 숨어 있습니다. constraint를 따르는 모든forall 타입을 받아 들여도 문제가 없다는 뜻입니다.

딱 부러지게 나온 구체 타입, 어떤 종류의 타입, 모든 타입… 이들 모두를 타입이라 표현하겠습니다. 무슨말이냐 하면 IO 타입, MonadIO => 타입, forall 타입 이들이 모두 그냥 같은 수준의 다른 타입들입니다. IO타입과 MonadIO => 는 서로 다른 타입입니다. MonadIO => 타입과 forall 타입은 서로 다른 타입입니다. 예를 들어, forall 타입을 받는 함수와, MonadIO => 타입을 받는 함수는 다른 타입의 함수입니다.

물론, 바이너리가 만들어질 때 IO타입처럼 구체 타입이 아닌 타입들은 언젠가 절차를 거쳐 구체 타입이 될 겁니다. 하지만, 에러를 읽을 때는 특별한 다음 절차를 내포했다는 건 잠시 잊어버리고 그냥 타입으로 바라봅니다. 에러에 등장하는 타입은

Int, Maybe Int, Strin, [], MonadIO => 타입, forall 타입….

모두 같은 선상에 놓인 타입이란 뜻입니다.

No instance - 인스턴스가 있는 타입으로 바꿔 봐

import Control.Monad.Reader

func1 :: m ()
func1 = do
  a <- ask
  liftIO $ print a

컴파일 하면

No instance for (Monad m) arising from a do statement
      Possible fix:
        add (Monad m) to the context of
          the type signature for:
            func1 :: forall (m :: * -> *). m ()
|   a <- ask

Ambiguous type variable ‘a0’ arising from a use of ‘ask’
      prevents the constraint ‘(MonadReader a0 m)’ from being solved.
      Relevant bindings include func1 :: m () (bound at test.hs:7:1)

|   a <- ask
No instance for (MonadIO m) arising from a use of ‘liftIO’
      Possible fix:
        add (MonadIO m) to the context of
          the type signature for:
            func1 :: forall (m :: * -> *). m ()
|   liftIO $ print a

Ambiguous type variable ‘a0’ arising from a use of ‘print’
      prevents the constraint ‘(Show a0)’ from being solved.
      Relevant bindings include a :: a0 (bound at test.hs:12:3)
|   liftIO $ print a
...

첫 번째 에레는,
do구문을 보고 나온 형태 (Monad m) 인스턴스가 없다.”
m은 특별히 결정한 형태가 없습니다. * -> * 카인드의 모든 타입입니다. 모든 m을 위한 Monad 인스턴스는 당연히 없습니다.

두 번째 에러는,
ask를 보고 나온 타입 변수 a0가 모호해서, 제약constraint (MonadReader a0 m)을 풀 수 없다.”
askMonadReader클래스의 메소드이니, (MonadReader a0 m => ) 타입 형태인데, a0가 뭔지 알 수 없다입니다. 이 것도 아무런 타입 제약이 없으니, 모든 m으로 결정하고, (MonadReader a0 m) 인스턴스가 없다는 No instance 에러가 나면 될 것 같은데, 왜 a0에 먼저 꽂혔을까요? 추측 타입 변수 순서대로 풀이resolve를 시도하는게 아닐까 추측합니다.

세 번째 에러는 첫 번째와 같은 종류로 (MonadIO m)이 없다는 에러고,
네 번째 에러는 두 번째와 같은 종류로 (Show a0)a0가 모호하다는 에러입니다.

GHC가 a0가 모호하지 않도록, 타입을 지정해 보겠습니다. 패턴 매칭할때 타입을 써주려면 ScopedTypeVariable 확장을 켜야 합니다. 참고 - 확장 ScopedTypeVariables

{-# LANGUAGE ScopedTypeVariables #-}
...
  a :: Int <- ask -- 패턴에 타입 지정
No instance for (MonadReader Int m) arising from a use of ‘ask’
In a stmt of a 'do' block: a :: Int <- ask
10 |   a :: Int <- ask

a0가 모호하지 않게 Int로 알려주니, 이제 예상했던 No instance 에러가 납니다.

그럼 위의 에러들의 해결책은 뭘까요? No instance라 했으니, 인스턴스를 정의하면 해결될 것 같은 메시지입니다. 하지만, “모든 m”타입의 인스턴스는 정의할 수 없습니다. 애시당초 클래스와 인스턴스는 ad hoc 다형성 스타일 요소로 특정 타입을 위한 동작을 정의할 때 씁니다. 이제 여기서 진짜 타입 추론의 목적이 드러납니다. 사실 askliftIO는 모든 타입에 쓸 수 있는 메소드들이 아닙니다. 그러니, 나중에 m으로 이들을 쓸 수 없는 타입이 들어오게 내버려 두면 당연히 문제가 생깁니다. 함수 서명만 보고도 안전하게 조립할 수 있도록 안전 장치를 해두어야 합니다. (모든 타입이란 말이 헛갈리지 않게 forall 타입이라 표현하겠습니다.) forall m타입을 위한 인스턴스가 없다는 말은, forall m을 위한 인스턴스를 만들어서 해결하는게 아니라, forall m 타입을 인스턴스가 있을만한 타입으로 바꾸어서 해결합니다. 해결책은 바로 constraint입니다.

import Control.Monad.Reader

func1 :: (MonadIO m, MonadReader c m, Show c) => m ()
func1 = do
  a <- ask
  liftIO $ print a

do 구문은 bind 함수가 가려져 있는데, MonadIO 인스턴스라면 bind 함수가 있을테니 지금 당장 구체 타입은 모르더라도 나중에 만나게 될 컨텍스트가 MonadIO m => 타입이기만 하면 됩니다. forall m 타입을 위한 인스턴스가 없으니, 나중에 구체 타입을 알아야 할 때가 오면 MonadIO m => 타입의 인스턴스를 꼭 환인해서 조립하란 표시입니다. 마찬가지로, ask를 쓰고 있으니, MonadReader 클래스의 인스턴스여야 하고, print를 쓰고 있으니 Show 클래스의 인스턴스여야 합니다. forall m타입을 (MonadIO, MonadReader, Show) => 타입으로 바꾸어 해결했습니다.

해결책을 정리하면
No instance 에러가 났다면, 진짜 필요한 인스턴스 정의가 없거나, 대상 타입이 잘 못 지정됐을 수 있습니다.
예) MonadIO m => 타입이어야 하는데 forall 타입으로 되어 있다.

※ mtl 스타일 참고 - 모나드 트랜스포머 그리고 mtl

Couldn’t match type - 하나의 대상이 두 가지 이상의 형태로 추론 됐다.

import Control.Monad.Trans.Reader
import Control.Monad.IO.Class
someFunc :: m ()
someFunc = do
  a <- ask
  liftIO $ print a
Couldn't match type ‘m’ with ‘ReaderT a0 m0’ ---- (1)
      ‘m’ is a rigid type variable bound by ----------- (2)
        the type signature for:
          func1 :: forall (m :: * -> *). m ()
      Expected type: m a0 ------------------------------(3)
        Actual type: ReaderT a0 m0 a0 ------------------(4)
In a stmt of a 'do' block: a <- ask          
Couldn't match type ‘m’ with ‘ReaderT a0 m0’
  1. m타입은 서명 forall (m :: * -> *). m ()에 지정된rigid 타입인데, mReaderT와 매치할 수 없다.”
      ‘m’ is a rigid type variable bound by
        the type signature for:
          func1 :: forall (m :: * -> *). m ()
  1. m타입 추론을 못해서 m으로 남아 있는게 아니라, 서명 forall (m :: * -> *). m ()에서 모든 타입이 될 수 있다고 지정한rigid 타입이라 따로 더 이상 추론하지 않습니다. 여기서 결정된rising 타입은 “어떤 타입이 들어와도 상관 없다”가 아니라 , 반드시 “모든 타입이 다 가능해야 한다”입니다. 여기선 카인드 조건이 들어가 있습니다. 완전히 모든 타입이 아니라 * -> * 카인드인 모든 타입이란 뜻입니다.

※ 카인드는 타입 분류입니다. 타입 변수 없이 자체로 타입으로 쓰일지, 타입 변수 하나를 받아 타입으로 쓰일지… 등에 따라 타입을 분류해 놓은 겁니다. Int는 * 카인드이고, Maybe는 * -> * 카인드입니다.

      Expected type: m a0
  1. 그래서 forall m 타입이 와야하는데,
 Actual type: ReaderT a0 m0 a0
In a stmt of a 'do' block: a <- ask
  1. ask의 결과 타입은 ReaderT a m 이므로, 여기서 결정된 타입은 ReaderT 입니다.

정리하면, 단서들을 통해 찾은 결과가 두 가지인데, 이 두가지 forall mReaderT가 매치되지 않는다는 에러입니다.

Could not deduce - 클래스 제약에 필요한 정보가 없어

someFunc :: (MonadIO m, MonadReader r m) => m ()
someFunc = do
  a <- ask
  liftIO $ print a
Could not deduce (Show r) arising from a use of ‘print’
      from the context: (MonadIO m, MonadReader r m)
        bound by the type signature for:
                   someFunc :: forall (m :: * -> *) r.
                               (MonadIO m, MonadReader r m) =>
                               m ()
8 |   liftIO $ print a

a타입을 다루는 Show 인스턴스가 있어야 하는데, a타입에 대한 정보가 없습니다.

문장 그대로 읽으면,
Could not deduce (Show r) arising from a use of ‘print’ from the context (MonadIO m, MonadReader r m)
print 함수를 쓰려면, rShow의 인스턴스여야 하는데, (MonadIO m, MonadReader r m) 컨텍스트만 가지곤 Show를 추론deduce할 수 없다는 뜻입니다.

  1. 단서로 나온 타입의 인스턴스가 없다. (보통 단서에서 나온 타입이 잘 못 됐다.) - No instance
  2. 단서1에서 나온rising 타입 형태와 단서2에서 나온 타입 형태가 맞지 않다. - Couldn’t match
  3. 단서에서 나온 클래스의 인스턴스만 쓰겠다는 정보가 Constraint에 없다. - Could not deduce

두 번째와 세 번째의 차이는 첫 번째 에러는 m, ReaderT 두 타입의 문제고, 두 번째 에러는 Show 클래스와 r 타입의 문제입니다. Constraint가 없을 때는 deduce 에러는 나지 않고 보통 No Instance 에러가 납니다.

2021.5.15 추가

요약하면

  1. No instance를 보면
    “타입까지 추론 됐다. 하지만 클래스 제약 조건에 맞는 인스턴스가 없는 타입이다.”
    “어떤 단서로는 클래스C로 추론했고, 어떤 단서로는 타입A로 추론했는데, 타입A를 키로하는 클래스C의 인스턴스는 없다.”

  2. Could’t match를 보면
    “타입이 두 가지 이상으로 추론됐다.”

  3. Could not deduce를 보면
    “코드에 클래스 제약이 있지만, 추론된 클래스와 관련된 내용이 없다.” (클래스 제약이 있을 때만 나는 에러입니다. 클래스 제약이 없다면 No instance 에러가 나고, 제약이 있긴 있는데 만족스럽지 못하면 Could not deduce 에러가 납니다.)

단서들로 추론된 결과가 구체 타입까지 추론되어야 하는 건 아닙니다.

  1. 하나의 특정 타입으로 추론될 수도
  2. 특정 클래스 까지만 추론될 수도 ( 타입 군이라 표현할 수도 있겠습니다.)
  3. 아예 모든 타입으로 추론될 수도 있습니다. ( 조건에 따라 다른 타입으로 변한다는 뜻이 아닙니다. 모든 타입을 처리할 수 있어야 한다는 말입니다.)

※ 거듭 강조하지만, 3번도 추론이 끝난 상태입니다.

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