如何解决如何使用可扩展效果获得“单子转换器的不灵活语义”?
考虑以下示例。
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
,即 { {1}}。
然而,在论文 “Extensible Effects: An Alternative to Monad Transformers” by Oleg Kiselyov,et al. 中,作者说这是“一个令人惊讶且不受欢迎的结果”。他们期望结果是 Right [7]
,因为 Right [5,1]
通过不重新抛出异常从异常中恢复。本质上,他们希望 handler
移动到 catchError
中,如下所示。
ex1
确实,这就是可扩展效果的作用。它们通过将效果处理程序移近效果源来更改程序的语义。例如,newtype TooBig = TooBig Int deriving Show
choose :: Monadplus m => [a] -> m a
choose = msum . map return
ex1 :: (Monadplus m,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
。该论文的作者将此称为可扩展效果相对于 monad 转换器的优势之一,声称 monad 转换器具有“不灵活的语义”。
但是,如果我希望结果是 throwError
而不是 Right [7]
怎么办?如上面的示例所示,可以使用 monad 转换器来获得这两种结果。但是,由于可扩展效果似乎总是将效果处理程序移近效果源,因此似乎不可能获得结果 Right [5,1]
。
那么,问题是如何使用可扩展的效果来获得“单子转换器的不灵活语义”?在使用可扩展效果时,是否可以防止单个效果处理程序靠近效果源?如果不是,那么这是需要解决的可扩展效应的限制吗?
解决方法
我也对那篇特定论文的摘录中的细微差别感到有些困惑。我认为这是更加有用采取退后几步,并解释代数影响了企业,到该文件属于背后的动机。
在MTL的做法在某种意义上是最明显和最普遍:你有一个接口(或“效应”),把它放在一个类型的类和收工。即一般性的成本,这是无原则的:你不知道当你把界面在一起会发生什么。这个问题表现得最具体,当你实现一个接口:你必须同时实现所有的人。我们这样想,每个接口都可以在隔离在一个专用的变压器来实现,但如果你有两个接口,比如说MonadPlus
和MonadError
,通过变压器实现ListT
和{{1} },为了撰写他们,你也必须要么执行ExceptT
的{{1}}或MonadError
的{{1}}。这为O(n ^ 2)实例问题被普遍理解为“只是样板”,但更深层次的问题是,如果我们允许接口是任何形状的,有没有讲述什么危险可能隐藏在“样板”,如果甚至可以在所有来实现。
我们必须在这些接口上放置更多结构。对于“升降机”的一些定义(ListT
从MonadPlus
),是我们可以通过变压器均匀地解除作用完全代数影响。 (也参见,Monad Transformers and Modular Algebraic Effects,What Binds Them Together。)
这是不是真正的限制。虽然一些接口不是在技术意义上的代数,如ExceptT
(由于lift
),它们通常还是可以的代数作用的框架内表达,只是少字面上。同时限制一个“接口”的定义中,我们也获得使用它们的丰富的方式。
所以我认为代数效果是所有之前的接口不同的思维,更精确的方式。作为一种思维方式,因此它可以在不改变你的代码,这就是为什么比较倾向于看相同的代码两次,这是很难看到这一点,而对周围的背景和动机把握什么采纳。如果您想为O(n ^ 2)情况下的问题是一个微不足道的“样板”的问题,你已经相信的原则是接口应该是组合的;代数效果是明确的设计围绕这一原则图书馆和语言的一种方式。
“代数效应”是没有固定定义一个模糊概念。如今它们是最知名的由语法设有MonadTrans
和MonadError
构建体(或catch
/ call
/ handle
/ op
和{{ 1}} / perform
)。 throw
是一个结构使用接口和raise
是我们如何实现它们。这个想法常见于这种语言是,有式(因此称为“代数”),提供一个基本的直觉如何catch
和match
的行为的方式,是独立于所述接口的,特别是通孔的相互作用call
具有顺序组合 handle
。
在语义上,一个程序的含义可以由call
是个树表示,并且一个handle
是这样的树的变换。这就是为什么 Haskell 中许多“代数效应”的化身都是从自由 monad 开始的,这些树类型由节点类型 handle
参数化:
(>>=)
从这个角度来看,程序call
是具有三个分支的树,与分支标记handle
在异常结束:
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
{效果{1}}。 -
要处理
[]
的影响,方法之一是使用组成一个Const Int
节点的所有子Const
,收集在一个最终列表他们的结果。这可以被看作是深度优先搜索的概括。
您得到的结果Free (Const x)
或h x
取决于这些转换的排序方式。
当然,在 MTL 方法中,monad 变换器的两个阶数是有对应关系的,但是程序作为树的直觉,通常适用于所有代数效应,当你在中间时就不那么明显了实施实例如[]
为Free [...]
。直觉可能是有意义后验,但它是先验混淆,因为型类的实例是像处理程序不一流值,和单子变压器来讲通常表示最后解释(隐藏在单子(>>=)
它们变换),而不是最初的语法。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。