에러 메시지를 문장 그대로 읽으려면 다음에 익숙해져야 합니다.
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 타입….
모두 같은 선상에 놓인 타입이란 뜻입니다.
import Control.Monad.Reader
func1 :: m ()
= do
func1 <- ask
a $ print a liftIO
컴파일 하면
No instance for (Monad m) arising from a do statement
• Possible fix:
Monad m) to the context of
add (type signature for:
the func1 :: forall (m :: * -> *). m ()
| a <- ask
Ambiguous type variable ‘a0’ arising from a use of ‘ask’
• MonadReader a0 m)’ from being solved.
prevents the constraint ‘(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:
MonadIO m) to the context of
add (type signature for:
the func1 :: forall (m :: * -> *). m ()
| liftIO $ print a
Ambiguous type variable ‘a0’ arising from a use of ‘print’
• Show a0)’ from being solved.
prevents the constraint ‘(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)
을 풀 수 없다.”
ask
는 MonadReader
클래스의 메소드이니, (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 다형성 스타일 요소로 특정 타입을 위한 동작을 정의할 때 씁니다. 이제 여기서 진짜 타입 추론의 목적이 드러납니다. 사실 ask
와 liftIO
는 모든 타입에 쓸 수 있는 메소드들이 아닙니다. 그러니, 나중에 m
으로 이들을 쓸 수 없는 타입이 들어오게 내버려 두면 당연히 문제가 생깁니다. 함수 서명만 보고도 안전하게 조립할 수 있도록 안전 장치를 해두어야 합니다. (모든 타입이란 말이 헛갈리지 않게 forall
타입이라 표현하겠습니다.) forall m
타입을 위한 인스턴스가 없다는 말은, forall m
을 위한 인스턴스를 만들어서 해결하는게 아니라, forall m
타입을 인스턴스가 있을만한 타입으로 바꾸어서 해결합니다. 해결책은 바로 constraint입니다.
import Control.Monad.Reader
func1 :: (MonadIO m, MonadReader c m, Show c) => m ()
= do
func1 <- ask
a $ print a liftIO
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
import Control.Monad.Trans.Reader
import Control.Monad.IO.Class
someFunc :: m ()
= do
someFunc <- ask
a $ print a liftIO
Couldn't match type ‘m’ with ‘ReaderT a0 m0’ ---- (1)
• type variable bound by ----------- (2)
‘m’ is a rigid type signature for:
the 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’
m
타입은 서명 forall (m :: * -> *). m ()
에 지정된rigid 타입인데, m
을 ReaderT
와 매치할 수 없다.”type variable bound by
‘m’ is a rigid type signature for:
the func1 :: forall (m :: * -> *). m ()
m
의 타입 추론을 못해서 m
으로 남아 있는게 아니라, 서명 forall (m :: * -> *). m ()
에서 모든 타입이 될 수 있다고 지정한rigid 타입이라 따로 더 이상 추론하지 않습니다. 여기서 결정된rising
타입은 “어떤 타입이 들어와도 상관 없다”가 아니라 , 반드시 “모든 타입이 다 가능해야 한다”입니다. 여기선 카인드 조건이 들어가 있습니다. 완전히 모든 타입이 아니라 * -> *
카인드인 모든 타입이란 뜻입니다.※ 카인드는 타입 분류입니다. 타입 변수 없이 자체로 타입으로 쓰일지, 타입 변수 하나를 받아 타입으로 쓰일지… 등에 따라 타입을 분류해 놓은 겁니다. Int는 *
카인드이고, Maybe는 * -> *
카인드입니다.
Expected type: m a0
forall m
타입이 와야하는데,Actual type: ReaderT a0 m0 a0
In a stmt of a 'do' block: a <- ask •
ask
의 결과 타입은 ReaderT a m
이므로, 여기서 결정된 타입은 ReaderT
입니다.정리하면, 단서들을 통해 찾은 결과가 두 가지인데, 이 두가지 forall m
과 ReaderT
가 매치되지 않는다는 에러입니다.
someFunc :: (MonadIO m, MonadReader r m) => m ()
= do
someFunc <- ask
a $ print a liftIO
Could not deduce (Show r) arising from a use of ‘print’
• : (MonadIO m, MonadReader r m)
from the contexttype signature for:
bound by the 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
함수를 쓰려면, r
이 Show
의 인스턴스여야 하는데, (MonadIO m, MonadReader r m)
컨텍스트만 가지곤 Show
를 추론deduce할 수 없다는 뜻입니다.
두 번째와 세 번째의 차이는 첫 번째 에러는 m, ReaderT 두 타입의 문제고, 두 번째 에러는 Show 클래스와 r 타입의 문제입니다. Constraint가 없을 때는 deduce 에러는 나지 않고 보통 No Instance 에러가 납니다.
2021.5.15 추가
요약하면
No instance를 보면
“타입까지 추론 됐다. 하지만 클래스 제약 조건에 맞는 인스턴스가 없는 타입이다.”
“어떤 단서로는 클래스C로 추론했고, 어떤 단서로는 타입A로 추론했는데, 타입A를 키로하는 클래스C의 인스턴스는 없다.”
Could’t match를 보면
“타입이 두 가지 이상으로 추론됐다.”
Could not deduce를 보면
“코드에 클래스 제약이 있지만, 추론된 클래스와 관련된 내용이 없다.” (클래스 제약이 있을 때만 나는 에러입니다. 클래스 제약이 없다면 No instance 에러가 나고, 제약이 있긴 있는데 만족스럽지 못하면 Could not deduce 에러가 납니다.)
단서들로 추론된 결과가 구체 타입까지 추론되어야 하는 건 아닙니다.
※ 거듭 강조하지만, 3번도 추론이 끝난 상태입니다.