GHC는 매개 변수parameter의 타입을 추론 단서로 조립할 코드를 찾습니다.
import Control.Monad.IO.Class ( MonadIO(..) )
import Control.Monad.Reader ( MonadReader, runReaderT, ask )
func :: (MonadReader cfg m, MonadIO m) => a -> m ()
= do
func _ <- ask
cfg $ putStrLn "ok"
liftIO
main :: IO ()
= do
main 1) "config" runReaderT (func
함수 선언부에 있는 제약 (MonadReader cfg m, MonadIO m) =>
는 뭘 의미할까요?
함수 리턴 타입 m 이 의미하는 건 뭘까요?
“함수내에서 제약에 있는 클래스의 메소드를 쓸테니, 나중에 코드 조립할 때 클래스의 인스턴스에서 구체 코드를 가져오면 돼” 란 뜻입니다.
class ClassA a where
method1 :: a -> a -> a
= a1
method1 a1 a2
instance ClassA Int where
= x
method1 x y
func :: Int
= method1 1 2
func
main :: IO ()
= do
main 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 ()
= do
func _ <- ask
cfg $ putStrLn "ok"
liftIO
main :: IO ()
= do
main 1) "config" runReaderT (func
Could not deduce (MonadIO m) arising from a use of ‘liftIO’
• : MonadReader cfg m
from the contexttype signature for:
bound by the 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
= id liftIO
MonadReader 클래스 정의
class Monad m => MonadReader r m | m -> r where
ask :: m r
= reader id
ask
local :: (r -> r) -> m a -> m a
reader :: (r -> a) -> m a
= do
reader f <- ask
r return (f r)
-- asks는 클래스 메소드가 아님에 주의해 주세요.
asks :: MonadReader r m
=> (r -> a) -> m a
= reader
asks
instance MonadReader r ((->) r) where
= id
ask = m . f
local f m = id reader
클래스의 네임스페이스
class ClassA a where
method1 :: a -> a -> a
= a1
method1 a1 a2
class ClassB a where
method1:: a -> a -> a
= a1 method1 a1 a2
위와 같이 메소드를 같은 이름으로 정의하면 Multiple declarations of ‘method1’
에러가 납니다. 클래스가 네임스페이스를 독립해서 가지고 있는게 아닙니다.↩︎