为了练习我的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
字符串
不幸的是,我得到了这个丑陋的嵌套代码,我在其中使用了多个do表达式。在我的理解中,使用monad和do表示法的主要原因之一是避免这种代码嵌套。例如,我可以使用Maybe monad来评估不同的解析步骤。(lexing,解析)而不需要单独检查每一步的成功。遗憾的是,在这个例子中这是不可能的,因为我需要使用诸如print和writeFile之类的函数,需要IO monad的函数与需要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
型
2条答案
按热度按时间nmpmafwu1#
首先,关于
do
符号的良好直觉!在本例中,您希望将Either String
monad与IO
monad合并组合在一起。结果将是一个新的monad,其中您将得到一个平坦的do
-块。(注意,你不需要Maybe
,因为Maybe
不允许你记录错误信息。)Either String
和IO
的组合单子被称为ExceptT String IO
,其中,ExceptT
是transformers
包中定义的以下类型(任何GHC安装都应附带此包)。字符串
你会想要和一个函数一起使用,
型
它用一个给定的错误消息来注解
Maybe
的“无信息”故障,还有一个函数,型
它“处理”
ExceptT String
效果,只留下IO
。您还需要该函数(在transformers
中定义)型
以将现有的
IO
操作放入这个新的monad中。型
另一种解决方案是使用一个函数,如
型
这利用了
IO
已经具有内置异常机制的事实。(我认为这是一个问题),这是一个行动可能产生的错误是不明确的类型了,因此没有实施结构良好的错误处理。(例如,请注意,我不再被迫使用像printingError
这样的函数来处理我的异常。异常只是冒泡,越过main
,并由运行时系统处理。)(NB:我没有测试过这个答案中的任何内容。如果有错误,请原谅。)
vddsk6oq2#
这实际上是你偶然发现的一个非常大的问题,在某种程度上,它仍然是一个活跃的研究领域。
问题很简单,如果我有一个
Functor f
和一个Functor g
,我可以得到一个Functor (Compose f g)
,字符串
也就是说,两个
Functor
的组合是一个Functor
。同样,如果我有一个
Applicative f
和一个Applicative g
,那么它们的组合也是一个Applicative
:型
然而,如果我有一个
Monad f
和一个Monad g
,那么我们并不清楚如何生成一个Monad (Compose f g)
。事实上,一般来说,两个单子的组合不是一个单子。我们当然可以尝试。型
但是我们不能填充
???
。无论我们如何尝试,一旦我们应用f
,我们将得到f (g (Compose f g b))
(直到同构,f (g (f (g b)))
,我们需要Compose f g b
(equiv,f (g b)
))。每个monad都可以
join
,也就是说,每个monad都可以将其自身的两层扁平化为一层型
但是我们有一个
f (g (f (g b)))
,想要一个f (g b)
。如果 * 只有 * 我们可以交换中间的g
和f
。我们可以型
从根本上说,找出哪些monad可以组合就是识别哪些
f
和g
可以进行中间操作。型
如果
g
是IO
,f
是一个纯单子(读作:不做IO
),那么这个签名是型
如果没有
unsafePerformIO
,这是不可能的,我也不会写一个依赖于unsafePerformIO
的Monad
示例。所以Compose f IO
一般不是Monad
(尽管Compose IO g
经常是一个)。这个问题有几种不同的解决方案。最简单的是monad transformers,由
transformers
包实现。transformers
稍微改变了框架。我们不是写“monad”,而是为monad写一种工厂,称为“monad transformer”。在没有外部包的vanilla Haskell中,我们只说Either e :: * -> *
是Monad
。型
在
transformers
下,我们将其 Package 在额外的间接层中。型
然后我们说,而
Either e :: * -> *
是一个Monad
,ExceptT e :: (* -> *) -> * -> *
是一个 monad Transformer。也就是说,给定一个monadm
,我们可以应用ExceptT e
来得到一个monadExceptT e m
,这个monad是在m
的 inside 上合成一个Either
的结果。它是一个从monad到monad的Map。我们可以使用与
Compose
相同的技术为ExceptT e m
获取Functor
和Applicative
示例。型
现在,当我们为我们的(尝试的)
Monad
示例写同样的东西时,我们说型
这个未知的
???
的类型是m (Either e (ExceptT e m b)) -> m (Either e b)
,我们实际上可以这样写。型
如果你想知道我是怎么想到
go
函数的,我建议你自己尝试一下,你想要一个m (Either e (ExceptT e m b)) -> m (Either e b)
类型的函数,所以只要开始尝试写它并遵循类型错误。所以现在我们有一种方法,给定一个任意的单子
m
,得到一个单子,它是m
* 加上 *Either
的效果,而ExceptT
是存在于transformers
中的一个真实的类型。如果你想将
Either e a
转换为ExceptT e m a
(也就是说,在不使用m
的情况下添加m
的效果),那就是except
。如果你想将m
转换为ExceptT e m a
,那就是lift
。所以你建议的main
函数看起来像这样型
现在,你可能已经注意到了,这有点冗长了。没有人想一直在
lift . lift . except
上运行他们所有的代码。这就是最前沿的地方:有几种不同的方法可以减少噪音。mtl
(monad Transformer库)定义了一堆类型类,它们“神奇地”插入lift
调用以使类型对齐。基本上,我们利用Haskell非常强大的类型推断和类型类解析系统来为我们插入样板。polysemy
使用了一个名为 free monads 的范畴论结构,本质上是一个名为Sem
的“超级monad”,它能够以一种非常抽象的方式表示效果。整本书都可以写关于如何使这符合人体工程学,但它都归结为你在这里提出的基本问题:“我有两个单子,我想把它们挤在一起,得到另一个单子”