클래스 제약 Class Constraint

Posted on April 13, 2021

GHC는 매개 변수parameter의 타입을 추론 단서로 조립할 코드를 찾습니다.

import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Reader ( MonadReader, runReaderT, ask )

func :: (MonadReader cfg m, MonadIO m) => a -> m ()
func _ = do
    cfg <- ask
    liftIO $ putStrLn "ok" 

main :: IO ()
main = do
    runReaderT (func 1) "config"

함수 선언부에 있는 제약 (MonadReader cfg m, MonadIO m) => 는 뭘 의미할까요?
함수 리턴 타입 m 이 의미하는 건 뭘까요?

  1. 파라메터가 소문자인 건 하스켈이 코드를 조립할 때 알아서 적당한 구체 타입으로 고른다입니다.
  2. 제약에 들어 있는 건 타입이 아니라 클래스입니다. 지금 정의하고 있는 함수내에서 그 클래스에 들어있는 메소드를 쓰겠다는 말입니다. 구체 인스턴스는 아직 모르더라도 말입니다.

“함수내에서 제약에 있는 클래스의 메소드를 쓸테니, 나중에 코드 조립할 때 클래스의 인스턴스에서 구체 코드를 가져오면 돼” 란 뜻입니다.

클래스까지 추론( 클래스 메소드를 어느 인스턴스에 있는 걸 쓸지 결정 )

class ClassA a where
    method1 :: a -> a -> a
    method1 a1 a2 = a1

instance ClassA Int where
    method1 x y = x

func :: Int
func = method1 1 2

main :: IO ()
main = do
    print $ func 
  1. func 안에서 method1을 썼는데, func의 리턴값이 Int이므로 method1의 리턴값도 Int로 추론
  2. method1의 매개 변수 타입은 리턴 타입과 같으므로 method1 :: Int -> Int -> Int 로 추론
  3. 그럼 instance ClassA Int 에 있는 method1을 실행하면 된다까지 추론합니다.

func 선언에는 ClassA 클래스 제약( func :: ClassA a => a )이 없는데, 왜 에러가 나지 않을까요? func의 리턴 타입을 보고 method1의 구체 타입을 알 수 있기 때문에 굳이 제약을 써주지 않아도 적당한 인스턴스를 찾을 수 있기 때문입니다.

클래스까지 추론한 것 같긴 한데 에러( 리턴 타입이 폴리모픽 )

Player => 타입 제약을 자동으로 추론해 주진 않아
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Reader ( MonadReader, runReaderT, ask )

--func :: (MonadReader cfg m ,MonadIO m) => a -> m ()
func :: (MonadReader cfg m)  => a -> m ()
func _ = do
    cfg <- ask
    liftIO $ putStrLn "ok" 

main :: IO ()
main = do
    runReaderT (func 1) "config"
Could not deduce (MonadIO m) arising from a use of ‘liftIO’
      from the context: MonadReader cfg m
        bound by the type signature for:
                   func :: forall cfg (m :: * -> *) a. MonadReader cfg m => a -> m ()

이 경우는 liftIO를 보고 MonadIO 클래스까지 추론한 것 같긴 한데 에러가 납니다. 무슨 차이일까요? 왜 여기서는 MonadIO 클래스까지 추론deduce하지 못한다고 할까요? 뭔지 모른다는 게 아니라 MonadIO 라고 콕 찝기까지 하는데 왜 추론하지 못할까요?

에러를 풀어 쓰면 “함수내에서 liftIO를 쓴다. liftIO를 쓰는 걸로 봐선 MonadIO m인데, MonadReader cfg m 컨텍스만 가지곤 MonadIO m만 받도록 할 수 없어” 입니다.

나중에 조립할 때 MonadIO m 만 받겠다는 정보를 넣어주면 해결됩니다.

func :: (MonadReader cfg m ,MonadIO m) => a -> m ()

추론된 정보들(타입 또는 제약)끼리 적절하게 만나고 있는지 확인

그럼 언제 m의 구체 타입이 추론될까요?

instance MonadIO IO
instance MonadIO m => MonadIO (IdentityT m)	 
instance MonadIO m => MonadIO (ListT m)	 
instance MonadIO m => MonadIO (MaybeT m)	 
instance MonadIO m => MonadIO (ContT r m)
...

이 많은 인스턴스 중 하나를 고르려면 m을 알아야 합니다. func 정의만 봐서는 m이 뭔지 알 수가 없습니다. 나중에 main에서 쓰인 걸 보면, 그 때서야 m이 IO구나 알수 있게됩니다. 그런데, 이 건 m을 IO로 추론했다고 말하기 보다, main에서 func의 리턴 타입을 IO로 결정한 후, func의 제약 사항에 들어 맞는지 확인하다고 말할 수 있습니다.

참고 - 타입 추론 포스트

클래스 제약의 실용적인 뜻

클래스 제약에 MonadIO m => 를 써주면, 지금은 클래스까지만 알려주고 나중에 구체 타입을 추론하라고 미루는 효과가 있습니다.

클래스 제약은 구체 타입 추론을 뒤로 미루기 위해 써주는 겁니다.

“구체 타입은 아직 뭔지 모르지만, 일단 OO클래스에 있는 메소드를 쓸테니, 나중에 코드 조립할 때 OO클래스의 인스턴스가 들어오는지 확인해” 입니다.

GHC가 되어 runReaderT (func "aa") "config" 코드를 해석해 보면 (소스에는 Reader c m 등으로 표기하는데, func의 리턴값 m과 혼동되지 않도록 Reader c innerm으로 표기했습니다.)

  1. func "aa"의 타입은 runReaderT를 먹이는 걸 봐서 ReaderT String innerm 타입일거야.
  2. func의 리턴 타입 mReaderT String innerm 타입으로 추론하고,
  3. askinstance Monad innerm => MonadReader r (ReaderT r innerm) 인스턴스에서 가져오면 되고,
  4. liftIOinstance MonadIO innerm => MonadIO (ReaderT r innerm) 인스턴스에서 가져오면 되고,
  5. innerm은 현재 main :: IO () 컨텍스트 있으니 IO 모나드로 추론해서,
  6. func의 리턴 타입 m ()ReaderT String IO () 으로 추론한다.

참고 - 모나드 트랜스포머 포스트, MonadIO1, MonadReader2, 클래스 네임스페이스3
http://learnyouahaskell.com/types-and-typeclasses


  1. MonadIO 클래스 정의

    class (Monad m) => MonadIO m where
        -- | Lift a computation from the 'IO' monad.
        liftIO :: IO a -> m a
    
    instance MonadIO IO where
        liftIO = id
    ↩︎
  2. MonadReader 클래스 정의

    class Monad m => MonadReader r m | m -> r where
        ask   :: m r
        ask = reader id
    
        local :: (r -> r) -> m a -> m a
        reader :: (r -> a) -> m a
        reader f = do
        r <- ask
        return (f r)
    
    -- asks는 클래스 메소드가 아님에 주의해 주세요.
    asks :: MonadReader r m
        => (r -> a) -> m a
    asks = reader
    
    instance MonadReader r ((->) r) where
        ask       = id
        local f m = m . f
        reader    = id
    ↩︎
  3. 클래스의 네임스페이스

    class ClassA a where
        method1 :: a -> a -> a
        method1 a1 a2 = a1
    
    class ClassB a where
        method1:: a -> a -> a
        method1 a1 a2 = a1

    위와 같이 메소드를 같은 이름으로 정의하면 Multiple declarations of ‘method1’ 에러가 납니다. 클래스가 네임스페이스를 독립해서 가지고 있는게 아닙니다.↩︎

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