如何在同一个 do 表达式中使用两个不同的 monad?

Aar*_*ron 6 monads haskell

为了练习我的 Haskell,我决定编写一个小的 JSON 解析器。在主文件中,我调用解析器的不同部分,打印结果,以便获得更多调试信息,然后将解析后的 JSON 写回到文件中:

{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Lexer as L
import qualified Parser as P
import qualified Printer as PR
import qualified Data.Text.Lazy.IO as TIO

main :: IO ()
main = do
    input  <- TIO.readFile "input.json"
    case L.tokenize input of
        Nothing -> putStrLn "Syntax error!"
        Just tokens -> do
            print tokens
            case P.parse tokens of
                Nothing -> putStrLn "Parse error!"
                Just parsedValue -> do
                    print parsedValue
                    TIO.writeFile "output.json" $ PR.toText parsedValue
Run Code Online (Sandbox Code Playgroud)

不幸的是,我得到了这个丑陋的嵌套代码,其中我在彼此内部使用了多个 do 表达式。根据我的理解,使用 monad 和 do-notation 的主要原因之一是避免这种代码嵌套。例如,我可以使用 Maybe monad 来评估不同的解析步骤(词法分析、解析),而无需单独检查每个步骤是否成功。遗憾的是,在本例中这是不可能的,因为我需要交替使用需要 IO monad 的 print 和 writeFile 等函数以及需要 Maybe monad 的函数。

我如何重构此代码以减少嵌套并包含更少的 do 表达式?或者更一般地说,我如何编写包含对不同 monad 函数的调用的干净代码?是否有可能在同一个 do 表达式中“混合”两个 monad,有点像这样?

main :: IO ()
main = do
    input  <- TIO.readFile "input.json"
    tokens <- L.tokenize input
    print tokens
    parsedValue <- P.parse tokens
    print parsedValue
    TIO.writeFile "output.json" $ PR.toText parsedValue
Run Code Online (Sandbox Code Playgroud)

HTN*_*TNW 4

首先,对do符号有良好的直觉!在这种情况下,您希望将Either Stringmonad 与IOmonad 组合在一起。结果将是一个新的 monad,您将在其中得到一个扁平的do块。(请注意,您不需要Maybe,因为Maybe它不允许您记录错误信息。)Either String和的组合 monadIO称为ExceptT String IO,其中ExceptT是包中定义的以下类型transformers(应该随 GHC 的任何安装一起提供)。

newtype ExceptT e m a = ExceptT (m (Either e a))
instance Monad m => Monad (ExceptT e m) -- and other instances
Run Code Online (Sandbox Code Playgroud)

您需要将它与类似的函数一起使用

orError :: Functor f => e -> f (Maybe a) -> ExceptT e f a
orError err x = ExceptT $ maybe (Left err) Right <$> x
Run Code Online (Sandbox Code Playgroud)

用给定的错误消息注释“无信息”失败Maybe,以及类似的函数

printingError :: ExceptT String IO () -> IO ()
printingError x = do
  result <- runExceptT x
  case result of
    Left err -> putStrLn err
    Right _  -> pure ()
Run Code Online (Sandbox Code Playgroud)

它“处理”ExceptT String效果并留下IO. 您还需要该函数(在 中定义transformers

lift :: IO a -> ExceptT e IO a
Run Code Online (Sandbox Code Playgroud)

将现有的操作融入IO到这个新的 monad 中。

main :: IO ()
main = printingError $ do
    input  <- lift $ TIO.readFile "input.json"
    tokens <- orError "Syntax error!" $ L.tokenize input
    lift $ print tokens
    parsedValue <- orError "Parse error!" $ P.parse tokens
    lift $ print parsedValue
    lift $ TIO.writeFile "output.json" $ PR.toText parsedValue
Run Code Online (Sandbox Code Playgroud)

另一种解决方案是仅使用类似的函数

orError :: String -> Maybe a -> IO a
orError err Nothing    = ioError $ userError err -- in System.IO.Error
orError err (Just ret) = pure ret

-- in which case
main :: IO ()
main = do
    input  <- TIO.readFile "input.json"
    tokens <- orError "Syntax error!" =<< L.tokenize input
    print tokens
    parsedValue <- orError "Parse error!" =<< P.parse tokens
    print parsedValue
    TIO.writeFile "output.json" $ PR.toText parsedValue
Run Code Online (Sandbox Code Playgroud)

IO这利用了已经具有内置异常机制的事实。然而,与此不同的是(我认为这是一个问题),操作可能产生的错误的类型不再明确,因此不会强制执行结构良好的错误处理。(例如,请注意,我不再被迫使用类似的函数来处理异常printingError。异常只是冒泡过去main并由运行时系统处理。)

(注意:我没有测试这个答案中的任何内容。如果有错误,抱歉。)