考虑以下示例。
newtype TooBig = TooBig Int deriving Show
choose :: MonadPlus m => [a] -> m a
choose = msum . map return
ex1 :: (MonadPlus m, MonadError TooBig m) => m Int
ex1 = do
x <- choose [5,7,1]
if x > 5
then throwError (TooBig x)
else return x
ex2 :: (MonadPlus m, MonadError TooBig m) => m Int
ex2 = ex1 `catchError` handler
where
handler (TooBig x) = if x > 7
then throwError (TooBig x)
else return x
ex3 :: Either TooBig [Int]
ex3 = runIdentity . runExceptT . runListT $ ex2
ex3
的值应该是多少是?如果我们使用 MTL 那么答案是 Right [7]
这是有道理的,因为 ex1
由于抛出错误而终止,并且 handler
简单地返回纯值 return 7
这是 Right [7]
.然而,在论文“Extensible Effects: An Alternative to Monad Transformers” by Oleg Kiselyov, et al.作者说这是“一个令人惊讶且不受欢迎的结果”。他们预计结果是
Right [5,7,1]
因为handler
通过不重新抛出异常从异常中恢复。本质上,他们预计 catchError
搬进ex1
如下。newtype TooBig = TooBig Int deriving Show
choose :: MonadPlus m => [a] -> m a
choose = msum . map return
ex1 :: (MonadPlus m, MonadError TooBig m) => m Int
ex1 = do
x <- choose [5,7,1]
if x > 5
then throwError (TooBig x) `catchError` handler
else return x
where
handler (TooBig x) = if x > 7
then throwError (TooBig x)
else return x
ex3 :: Either TooBig [Int]
ex3 = runIdentity . runExceptT . runListT $ ex1
事实上,这就是可扩展效应的作用。它们通过将效果处理程序移近效果源来更改程序的语义。例如,local
移近 ask
和 catchError
移近 throwError
.该论文的作者将此称为可扩展效果优于 monad 转换器的优势之一,声称 monad 转换器具有“不灵活的语义”。但是,如果我希望结果是
Right [7]
怎么办?而不是 Right [5,7,1]
无论出于什么原因?如上面的示例所示,可以使用 monad 转换器来获得这两种结果。然而,因为可扩展的效果似乎总是将效果处理程序移近效果源,所以似乎不可能得到结果 Right [7]
.那么,问题是如何使用可扩展的效果来获得“单子(monad)转换器的不灵活语义”?在使用可扩展效果时,是否可以防止单个效果处理程序靠近效果源?如果不是,那么这是需要解决的可扩展效果的限制吗?
最佳答案
我也对那篇特定论文的摘录中的细微差别感到有些困惑。我认为退后几步并解释该论文所属的代数效应企业背后的动机更有用。
MTL 方法在某种意义上是最明显和最通用的:你有一个接口(interface)(或“效果”),把它放在一个类型类中并称之为一天。这种通用性的代价是它是无原则的:你不知道将接口(interface)组合在一起会发生什么。当您实现一个接口(interface)时,这个问题最具体地出现:您必须同时实现所有这些。我们喜欢认为每个接口(interface)都可以在专用转换器中单独实现,但如果您有两个接口(interface),比如 MonadPlus
和 MonadError
,由转换器实现 ListT
和 ExceptT
,为了组合它们,您还必须实现 MonadError
为 ListT
或 MonadPlus
为 ExceptT
.这个 O(n^2) 实例问题通常被理解为“只是样板”,但更深层次的问题是,如果我们允许接口(interface)具有任何形状,那么不知道在该“样板”中可能隐藏什么危险,如果它甚至可以实现。
我们必须在这些接口(interface)上放置更多结构。对于“提升”的某些定义(来自 lift
的 MonadTrans
),我们可以通过变压器均匀提升的效应正是代数效应。 (另见,Monad Transformers and Modular Algebraic Effects, What Binds Them Together。)
这并不是真正的限制。虽然有些接口(interface)在技术意义上不是代数的,例如 MonadError
(因为 catch
),它们通常仍然可以在代数效应的框架内表达,只是不太像字面意思。在限制“接口(interface)”定义的同时,我们也获得了更丰富的使用方式。
所以我认为代数效应首先是一种不同的、更精确的界面思考方式。作为一种思维方式,因此可以在不更改代码的任何内容的情况下采用它,这就是为什么比较往往会查看相同的代码两次,并且如果不了解周围的上下文和动机就很难看出重点。如果您认为 O(n^2) 实例问题是一个微不足道的“样板”问题,那么您已经相信接口(interface)应该是可组合的原则;代数效应是围绕该原则明确设计库和语言的一种方式。
“代数效应”是一个没有固定定义的模糊概念。如今,它们最容易通过以 call
为特色的语法来识别。和一个 handle
构造(或 op
/perform
/throw
/raise
和 catch
/match
)。 call
是使用接口(interface)和 handle
的一种构造是我们如何实现它们。这些语言的共同想法是,有方程(因此是“代数”)提供了如何 call
的基本直觉。和 handle
以一种独立于界面的方式运行,特别是通过 handle
的交互具有顺序组合(>>=)
.
从语义上讲,程序的含义可以用 call
树表示。 s 和 handle
是这种树的改造。这就是为什么 Haskell 中许多“代数效应”的化身都是从自由 monads 开始的,树的类型由节点类型参数化 f
:
data Free f a
= Pure a
| Free (f (Free f a))
从这个角度来看,程序ex2
是一棵具有三个分支的树,分支标记为 7
以异常结尾:ex2 :: Free ([] :+: Const Int) Int -- The functor "Const e" models exceptions (the equivalent of "MonadError e")
ex2 = Free [Pure 5, Free (Const 7), Pure 1]
-- You can write this with do notation to look like the original ex2, I'd say "it's just notation".
-- NB: constructors for (:+:) omitted
和每个效果[]
和 Const Int
对应于转换树的某种方式,从树中消除这种影响(可能引入其他人,包括它自己)。Const
转换效果Free (Const x)
节点进入一些新树 h x
.[]
效果,一种方法是组合 Free [...]
的所有子级节点使用 (>>=)
,将他们的结果收集到最终列表中。这可以看作是深度优先搜索的推广。你得到的结果
[7]
或 [5,7,1]
取决于这些转换的排序方式。当然,在 MTL 方法中,monad 转换器的两个阶是对应的,但是程序作为树的直觉,通常适用于所有代数效应,当你正在实现一个实例如
MonadError e
为 ListT
.这种直觉在后验上可能有意义,但它是先验混淆的,因为类型类实例不是像处理程序那样的一流值,并且 monad 转换器通常根据最终解释来表示(隐藏在 monad m
中,它们会转换)而不是初始语法。
关于haskell - 如何使用可扩展效果获得 “inflexible semantics of monad transformers”?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65590600/