为了练习我的 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)
首先,对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并由运行时系统处理。)
(注意:我没有测试这个答案中的任何内容。如果有错误,抱歉。)