오늘은 최근에 Scotty 웹 프레임워크 버전을 올리면서 생긴 몇 가지 문제에 대해 나눠보려고 합니다. Scotty 0.20 부터 에러 처리하는 부분이 달라졌습니다. 그래서 ScottyT 시그니처가 ScottyT e m ()
에서 ScottyT m ()
로 바뀌었습니다.
새로운 에러 처리 방법은 다음에 다루겠습니다. 에러 처리가 바뀌면서 m
컨택스트에 새로운 제약이 생겼습니다. m
에 MonadUnliftIO
제약이 생겼습니다. MonadUnliftIO
는 liftIO
를 쓰기 어려운 인자로 받은 함수에서 liftIO
를 쓸 수 있도록 만든 타입 클래스인데요. 이것도 다음에 간단히 다루겠습니다. :)
중요한 것은 MonadUnliftIO
는 unliftio-core
패키지에 있고 ReaderT
에 대한 인스턴스 구현이되어 있습니다. 그래서 ScottyT
를 쓸 때 ReaderT
모나드 스택은 쉽게 쓸 수 있습니다.
하지만 제가 작성한 애플리케이션은 StateT
모나드 스택을 쓰고 있었습니다. 최상위 모나드 스택으로 자주 쓰지 않는 형태이지만 중간에 값을 바꿔서 쓰는 부분이 있어 StateT
를 사용했는데 MonadUnliftIO
의 StateT
인스턴스를 직접 구현하려니 복잡하고 ReaderT
를 최상위 스택으로 쓰는 것이 좋을 것 같아 ReaderT
에 들어 있는 리소스를 중간에 바꿀 수 있는지 찾아봤습니다. 다행히 transformers
패키지에 Control.Monad.Trans.Reader
모듈에 withReaderT
라는 함수가 있습니다.
withReaderT :: (r' -> r) -> ReaderT r m a -> ReaderT r' m a
withReaderT
는 다음과 같이 사용합니다. Relude
를 사용한다면 따로 더 import
할 모듈은 없습니다.
readerApp :: ReaderT Text IO ()
readerApp = do
x <- ask
liftIO $ print x
withReaderT (const ("World" :: Text)) $ do
y <- ask
liftIO $ print y
pass
z <- ask
liftIO $ print z
pass
main :: IO ()
main = do
runReaderT readerApp "Hello"
pass
위 프로그램의 출력 결과는 다음과 같습니다.
"Hello"
"World"
"Hello"
readerApp
에서는 withReaderT
를 사용해서 원래 ReaderT
리소스 값인 Hello
대신 World
가 됩니다. 하지만 App
을 Final Tagless 형식으로 쓰면 withReaderT
를 쓸 수 없습니다. 다행히 임의의 m
모나드에서 쓸 수 있도록 withReaderT
의 일반화 버전인 local
이라는 함수가 Control.Monad.Trans.Reader
모듈에 있습니다.
local :: (r -> r) -> ReaderT r m a -> ReaderT r m a
withReaderT
와 조금 다른 점은 r
타입이 같아야 한다는 것입니다. 위에서 만든 예제는 원래 리소스 타입이 Text
이고 withReaderT
안에서 쓰는 리소스 타입도 Text
이기 때문에 그대로 바꿔서 쓸 수 있습니다.
newtype App a = App {runApp :: ReaderT Text IO a}
deriving newtype (Functor, Applicative, Monad, MonadReader Text)
app :: (MonadReader Text m, MonadIO m) => m ()
app = do
x <- ask
liftIO $ print x
local (const "World") $ do
y <- ask
liftIO $ print y
pass
z <- ask
liftIO $ print z
pass
main :: IO ()
main = do
runReaderT app "Hello"
pass
이렇게 하면 State
모나드를 쓰지 않아도 local
범위에서 Reader
모나드에 있는 값을 바꿔 쓸 수 있습니다.