이 글을 읽으시려면 Monad Transformer를 먼저 알고 있어야 합니다.
참고 - 모나드 트랜스포머 포스트
클래스와 인스턴스를 사용하면 선언과 구현을 분리 할 수 있습니다. 먼저 선언해서 사용하고, 실제 구현체는 나중에 정해 줄 수 있습니다.
newtype Player = Player { name :: String }
newtype Villain = Villain { nickname :: String }
showNameDirectParam :: Player -> IO ()
= putStrLn $ "Player : " ++ (name p) showNameDirectParam p
함수에서 Player 타입을 다이렉트로 받으면, 이 함수가 Player 타입에 종속됐다고 말합니다.
이렇게 직접적으로 타입을 넘기지 않고, 클래스와 constraint를 이용하면
class HasName a where
getName :: a -> String
showName :: HasName a => a -> IO ()
= putStrLn $ getName x showName x
일단 showName
내에서는 getName
으로 name
값을 가져올거라 전제하고, 코드에서 마음대로 활용하고, 실제 구현체는 나중에 인스턴스를 통해 만들어 주면 됩니다.
instance HasName Player where
= "Player : " ++ name x
getName x
instance HasName Villain where
= "Villain : " ++ nickname x getName x
이렇게 클래스를 활용하는 걸 Has type class 패턴이라 부릅니다.
정리하면,
Player -> IO ()
는 Player
타입만 받아 들이지만,
HasName a => a
는 a가 뭐든간에 HasName 인스턴스이기만 하면 받아 들일 수 있습니다.
여기서 쓰인 아이디어를 모나드 트랜스포머와 붙여서 사용할 수 있습니다.
newtype AppM a = ReaderT Env (LoggingT IO) a
이렇게 몇 겹으로 쌓은 모나드 트랜스포머를 쓸 때, 특정 층에 있는 모나드의 메소드를 쓴다고 생각해 봅시다. ReaderT
가 어느 층에 있는지, LoggingT
가 어느층에 있는지 알아야 합니다. lift
를 몇 번을 써서 해당 층에 도달할지 다 기억한다는 건 매우 비효율적입니다. 이럴 때를 위해 mtl이 필요합니다.
모나드 트랜스포머를 여러겹 쌓은 걸 모나드 트랜스포머 스택이라고 합니다. 소스상 모양은 품고 품은 모양인데, 이 바닥 사람들은 수직으로 쌓아 올린 메타포를 즐겨 사용하나 봅니다. AppM
의 경우 가장 아래 IO
, 그 위에 LoggingT
, 그 위에 ReaderT
를 쌓아 올린 스택으로 바라봅니다. 그래서 IO모나드에 있는 액션을 AppM
층에서 사용하려면, 끌어 올린다lift는 표현을 씁니다.
-- m a 액션을 t m a 액션으로 끌어 올려 줍니다.
class MonadTrans t where
lift :: Monad m => m a -> t m a
많이 쓰이는 대부분의 트랜스포머(ListT, MaybeT, ExceptT, ReaderT …)들은 모두 MonadTrans의 인스턴스로 라이브러리에 미리 정의되어 있습니다.
Control.Monad.Trans.Class
트랜스포머 안에 들어있는 모나드의 액션을 실행하려면 lift를 써서 트랜스포머층으로 끌어 올립니다.
mtl에는, Monad Transformer만 있는게 아니라, Monad Transformer와 같이 쓸 라이브러리들(MonadReader, MonadState, MonadIO, …)도 같이 있습니다.
그 중 MonadReader를 살펴 보겠습니다.
class Monad m => MonadReader r m | m -> r where
ask :: m r
= reader id ask
| m -> r
functional dependency 문법은 각주 참고 1
Has클래스에서 봤던 아이디어처럼, 한 가지 타입만 ask를 쓸 수 있는게 아니라, MonadReader r m => ... constraint
를 만족하는 여러 타입들도 ask를 쓸 수 있게 됩니다.
Control.Monad.Trans.Reader #ask
여기 ask
는 클래스의 메소드로 정의되어 있는 게 아닙니다.
Control.Monad.Trans.Reader 모듈의 일반 함수입니다.
ask :: (Monad m) => ReaderT r m r ----- (1)
= ReaderT return ask
ReaderT 액션이므로 runReaderT
로 return
을 벗기고 r
을 넣어주면
return r
이되고
보통 env <- ask
를 쓰니 <-
로 return
으로 씌운 생성자가 벗겨지면 r
만 env
로 들어갑니다.
하지만 이 ask는 ReaderT 컨텍스트에서만 사용할 수 있습니다.
MonadReader 클래스는 ask 메소드를 가지고 있습니다.
class Monad m => MonadReader r m | m -> r where
ask :: m r ------- (2)
= reader id ask
그리고, 많이 쓰이는 트랜스포머들의 인스턴스를 미리 정의해 두었습니다.
import qualified Control.Monad.Trans.Reader as ReaderT (ask, local, reader)
...
instance Monad m => MonadReader r (ReaderT r m) where
= ReaderT.ask ask
오른 쪽의 ask
는 위의 (1)번 ReaderT의 ask
입니다.
다른 인스턴스를 보면
instance MonadReader r m => MonadReader r (MaybeT m) where
= lift ask ask
여기서 오른쪽 ask
는 어떤 ask
일까요? m
의 ask
입니다.
m
이 구체적으로 뭔지 몰라도 MonadReader r m
인스턴스가 있다는 것만 압니다.
함수를 ReaderT
타입을 직접적으로 받게 하지 않고, 다음처럼 constraint를 활용하면
-- m이 모나드인지는 어떻게 알까요? MonadReader 클래스 서명을 보면 Monad m => 이 있습니다.
someFunc :: MonadReader r m => Int -> m
= do
someFunc n
ask... n
이 함수는 딱 ReaderT만 받는게 아니라, 어떤 모나드든 MonadReader2 인스턴스이기만 하면 다 받을 수 있습니다. 그런데, 실용 프로그램에서 자주 만나게 되는 모나드 트랜스포머용 인스턴스를 미리 선언해 뒀기 때문에, 대부분의 경우 모나드 트랜스포머 스택 어느 층에든 ReaderT만 있으면 받을 수 있는 함수가 됩니다. 이 부분은 모나드 트랜스포머 포스트에 자세히 적어 두었습니다.
functional dependencies
짧게 동작만 설명하면, 매개 변수가 m, r 두 개지만, m이 정해지면 그에 따라 r도 정해진다는 문법입니다. 타입 추론을 거들기 위한 문법입니다.
필요한 이유 - Why is FunctionalDependency needed for defining MonadReader? - stackoverflow↩︎