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 이 의미하는 건 뭘까요?
“함수내에서 제약에 있는 클래스의 메소드를 쓸테니, 나중에 코드 조립할 때 클래스의 인스턴스에서 구체 코드를 가져오면 돼” 란 뜻입니다.
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 method1 :: Int -> Int -> Int 로 추론func 선언에는 ClassA 클래스 제약( func :: ClassA a => a )이 없는데, 왜 에러가 나지 않을까요? func의 리턴 타입을 보고 method1의 구체 타입을 알 수 있기 때문에 굳이 제약을 써주지 않아도 적당한 인스턴스를 찾을 수 있기 때문입니다.
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으로 표기했습니다.)
func "aa"의 타입은 runReaderT를 먹이는 걸 봐서 ReaderT String innerm 타입일거야.func의 리턴 타입 m을 ReaderT String innerm 타입으로 추론하고,ask는 instance Monad innerm => MonadReader r (ReaderT r innerm) 인스턴스에서 가져오면 되고,liftIO는 instance MonadIO innerm => MonadIO (ReaderT r innerm) 인스턴스에서 가져오면 되고,innerm은 현재 main :: IO () 컨텍스트 있으니 IO 모나드로 추론해서,func의 리턴 타입 m () 는 ReaderT String IO () 으로 추론한다.참고 - 모나드 트랜스포머 포스트, MonadIO1, MonadReader2, 클래스 네임스페이스3
http://learnyouahaskell.com/types-and-typeclasses
MonadIO 클래스 정의
class (Monad m) => MonadIO m where
-- | Lift a computation from the 'IO' monad.
liftIO :: IO a -> m a
instance MonadIO IO where
liftIO = idMonadReader 클래스 정의
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클래스의 네임스페이스
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’ 에러가 납니다. 클래스가 네임스페이스를 독립해서 가지고 있는게 아닙니다.↩︎