타입 추론 과정 따라가기

Posted on April 22, 2021

소문자로 쓰여 있는 건, 무조건 나중에 “필요한 만큼” 구체 타입으로 추론됩니다.

import Control.Monad.Reader
import Data.Function

func1 :: m ()
func1 = do
  cfg <- ask
  return ()

아래와 같은 에러가 납니다.

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 ()
...
8 |   cfg <- 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 ()
      Probable fix: use a type annotation to specify what ‘a0’ should be.
      These potential instances exist:
        instance [safe] Monad m => MonadReader r (ReaderT r m)
          -- Defined in ‘Control.Monad.Reader.Class’
        ...plus 13 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
...
8 |   cfg <- ask

문장 그대로 읽으면 do구문을 보고 instance (Monad m)을 찾아야 하는데, 그런 인스턴스가 없다는 얘기입니다. func1do를 쓰니 Monad입니다. m은 모든 타입이니 당연히 Monad m 인스턴스란 건 없습니다. 왜 Monad mcould not deduce라고 하지 않고, No instance 에러라고 했을까요? 여기선 mdeduce하지 못한게 아닙니다. 아무 제약constraint없이 m이라고만 쓰면 forall m. m 이란 뜻으로 모든 타입을 다 받을 수 있다고 결정된겁니다. 이렇게 찾은 mMonad 인스턴스는 없습니다. 그런데 만일 여기에 Constraint가 있다면, Constraint를 이용해서 타입 추론deduce을 시도 합니다. 보통 Constraint가 있어 deduce시도를 해봤는데도 필요한 타입을 찾지 못하면 could not deduce 에러가 납니다. 아래와 같이 Constraint를 일부 추가하고

func1 :: (Monad m) => m ()

다시 컴파일 하면,

Could not deduce (MonadReader a0 m) arising from a use of ‘ask’
      from the context: Monad m
        bound by the type signature for:
                   func1 :: forall (m :: * -> *). Monad m => m ()
      The type variable ‘a0’ is ambiguous
      Relevant bindings include
        func1 :: m () (bound at ...)
      These potential instances exist:
        instance [safe] Monad m => MonadReader r (ReaderT r m)
          -- Defined in ‘Control.Monad.Reader.Class’
        ...plus 13 instances involving out-of-scope types
        (use -fprint-potential-instances to see them all)
...
8 |   cfg <- ask

ask를 쓰는 걸로 봐서 mMonadReader a m 인스턴스일텐데, 서명의 Constraint에는 Monad m만 있을 뿐, 더 추론할 정보가 없습니다.

func1 :: (Monad m, MonadReader c m) => m ()

이렇게 하면 컴파일 성공입니다. 함수 내부에서 특정 타입을 쓰도록 바꾸고 타입 추론을 더 살펴 보겠습니다.

func1 :: (Monad m, MonadReader c m) => m a
func1 = do
  cfg <- ask
  return $ cfg + 1
Couldn't match type ‘c’ with ‘a’
        arising from a functional dependency between constraints:
MonadReader a m’
            arising from a use of ‘ask’
MonadReader c m’
            arising from the type signature for:
                           func1 :: forall (m :: * -> *) c a. (Monad m, MonadReader c m) => m a
      ‘c’ is a rigid type variable bound by
        the type signature for:
          func1 :: forall (m :: * -> *) c a. (Monad m, MonadReader c m) => m a
      ‘a’ is a rigid type variable bound by
        the type signature for:
          func1 :: forall (m :: * -> *) c a. (Monad m, MonadReader c m) => m a
8 |   cfg <- ask

ac는 rigid type입니다. 참고 - rigid type variable
forall c, 즉 모든 타입의 a와 c로 정해졌습니다.1 서명에 a와 c는 어떤 제약도 없으므로 어떤 타입이든 들어 올 수 있습니다. 함수 내부에서도 어떤 타입이 들어와도 처리할 수 있어야 합니다.

class Monad m => MonadReader r m | m -> r where
ask :: m r 

askm r 타입입니다. ask의 결과값은 func1 함수의 결과값과 타입이 같을테니 m a일 겁니다. 함수내에서는 ra가 같은 값으로 처리되고 있지만, 서명에는 둘 이 같다는 정보가 없습니다. MonadReader 서명을 보면 m -> rm이 정해지면 r은 따라서 고정되는 functional dependency가 있습니다. rm이 바뀌지 않으면 고정입니다. 무슨 말이냐 하면, mIO로 추론되고 r이 한 번 Int로 추론 됐다면, MonadReader Int IO로 추론되고, 한 컨텍스트 안에서 MonadReader Bool IO 같은 타입으로 추론할 수 없습니다. 에러를 그대로 번역하면

MonadReader r m에서 r은 타입 서명에 있는 제약에서 c로 한 번 추론됐으니, 결과 타입때문에 추론된 MonadReader a ma와 매칭 할 수 없다는 얘기입니다.

그럼 GHC의 추론에 의존하지 않고, rInt로 고정해 봅시다.

func1 :: (Monad m, MonadReader Int m) => m Int
Non type-variable argument in the constraint: MonadReader Int m
      (Use FlexibleContexts to permit this)
In the type signature:
        func1 :: (Monad m, MonadReader Int m) => m Int
6 | func1 :: (Monad m, MonadReader Int m) => m Int

제약constraint에는 구체 타입을 적어 줄 수 없습니다. 구체 타입 지정을 위해 언어 확장을 켭니다. 참고 FlexibleContexts

{-# LANGUAGE FlexibleContexts #-}

타입 서명과 함수 내부에서 쓰는 타입이 일치해야 합니다.

  1. Int를 받는다고 해놓고, 함수 내부에서는 Double이 필요한 코드가 있을 때
  2. 서명에서는 아무거나 받는다고 해놓고, 내부에서는 특정 타입만 가능한 코드로 되어 있을 때

이러면 Couldn’t match type 에러가 납니다. 1번은 별 어려움이 없는 말이지만, 2번은 가끔 혼동을 줄 때가 있습니다. 어려운 명제로 얘기하지 않고, 코드 실행 입장으로 생각하면 쉽게 이해가 갑니다. 서명에는 아무 타입, 내부에서는 MonadReader 인스턴스만 가능하다고 하면, GHC가 서명만 보고 코드 조립을 성사(컴파일)시켜 놓으면, 나중에 문제가 생길거라 추측할 수 있습니다.


  1. arising from
    No instance for (Monad m) arising from a do statement .. Ambiguous type variable a0 arising from a use of 'ask'이렇게 deduced from 을 쓰지 않고 arising을 쓰는 이유는 deduced 해서 나온 정보일 수도 있고, specified 해서 나온 정보일 수도 있기 때문인 것 같습니다. 번역하자면 do 구문에서 나온 (Monad m) 인스턴스가 없어서, ask에서 나온 타입 변수 a0가 뭔지 몰라서 입니다.↩︎

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