Reader 모나드 값을 중간에 바꾸기

Eunmin Kim·2024년 12월 4일
1

오늘은 최근에 Scotty 웹 프레임워크 버전을 올리면서 생긴 몇 가지 문제에 대해 나눠보려고 합니다. Scotty 0.20 부터 에러 처리하는 부분이 달라졌습니다. 그래서 ScottyT 시그니처가 ScottyT e m ()에서 ScottyT m ()로 바뀌었습니다.

새로운 에러 처리 방법은 다음에 다루겠습니다. 에러 처리가 바뀌면서 m 컨택스트에 새로운 제약이 생겼습니다. mMonadUnliftIO 제약이 생겼습니다. MonadUnliftIOliftIO를 쓰기 어려운 인자로 받은 함수에서 liftIO를 쓸 수 있도록 만든 타입 클래스인데요. 이것도 다음에 간단히 다루겠습니다. :)

중요한 것은 MonadUnliftIOunliftio-core 패키지에 있고 ReaderT에 대한 인스턴스 구현이되어 있습니다. 그래서 ScottyT를 쓸 때 ReaderT 모나드 스택은 쉽게 쓸 수 있습니다.

하지만 제가 작성한 애플리케이션은 StateT 모나드 스택을 쓰고 있었습니다. 최상위 모나드 스택으로 자주 쓰지 않는 형태이지만 중간에 값을 바꿔서 쓰는 부분이 있어 StateT를 사용했는데 MonadUnliftIOStateT 인스턴스를 직접 구현하려니 복잡하고 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 모나드에 있는 값을 바꿔 쓸 수 있습니다.

profile
Functional Programmer @Constacts, Inc.

0개의 댓글

관련 채용 정보