一个没有Monad变换器的明确Monad示例是什么?

17

单子变换器是所有标准单子(Reader、Writer、State、Cont、List等)的已知概念,但每个单子变换器的工作方式略有不同。没有通用的方法或公式可以根据具有单子实例的类型构造一个单子变换器。因此,不能保证根据某些任意业务要求设计的单子数据类型将具有单子变换器。是否有这样一个明确的例子?

相关工作

另一个问题解释说两个单子的函子组合不一定是单子。请参见此问题。这些示例并未回答当前问题,它们只是说明了构造单子变换器没有通用方法的问题。这些示例表明,给定两个单子M和N,我们有时会发现M(N a)是单子,有时N(M a)是单子,有时两者都不是单子。但这既不显示如何为M或N构造单子变换器,也不显示它是否存在。

另一个问题的答案认为IO单子不能有单子变换器,因为如果它有一个IOT,我们可以将IOT应用于List,然后将空列表(lift [])提升到结果单子中,必须撤消IO单子“先前”执行的副作用。这个论点基于这样一个想法,即IO单子“实际执行”副作用,假定这些副作用不能被撤消。然而,IO单子不是一个显式的类型构造函数。

讨论

在每个明确给出单子类型的示例中,总能找到某种单子变换器 - 有时需要一定的创造力。例如,存在于Haskell库中的ListT变换器最近相对较近地发现以微妙的方式不正确,但通过改变ListT的定义最终解决了这个问题。

没有转换器的单子的标准例子是像IO这样的单子,实际上并没有由显式类型构造函数定义 - IO是由库在低级别上以某种方式定义的不透明“魔法”类型。很明显,不能用纯函数给出单子实例来定义IO作为显式类型构造函数。 IO示例表明,如果我们允许单子实例包含具有不纯副作用的隐藏低级代码,则单子转换器可能不存在。因此,让我们将注意力限制在使用纯函数实现的单子上。

似乎没有算法可以从单子源代码自动派生单子转换器。我们甚至知道这总是可能的吗?

为了更清楚地说明我所说的单子的“显式示例”,假设我声称

 type Q u v a = ((u -> (a, Maybe a)) -> v) -> u -> (a, Maybe a)

我可以为类型参数a提供一个合法的Monad实例,并为Q u v的Monad实例生产纯函数return和join的源代码。那么我们是否知道Q u v有一个Monad变换器QT u v,使得QT u v Id等价于Q u v,并且Monad变换器的规律成立?我们是否知道如何显式构建QT?我不知道。
为了解决这个问题,我们需要:
  • 展示一种算法,能够从任意给定的类型构造器和一个给定的monad实例的实现中找到一个单子变换器;例如,给定代码type F a = r -> Either (a, a) (a, a, Maybe a)和这个类型的monad实例的实现,找到单子变换器的代码;为了简便起见,让我们将自己限制在由任何组合的->、元组和Either构成的类型构造器上;或者
  • 展示一个反例:一个显式的monad类型构造器,由显式的代码定义给出,例如type F a = r -> Either (a, a, a) (a, a, Maybe a)或其他任何类型,使得这是一个合法的Monad,具有由纯函数给出的Monad实例,但我们可以证明F没有单子变换器。

3
IO本身并不神奇,仅仅是它所包装的状态。newtype IO a = IO (状态 -> (状态, a)) - chepner
6
@chepner,实际上是这样的,因为状态传递 "语义" 无法模拟其某些特性,特别是 forkIO。当然,你可以认为 magic 的魔法也处理线程,但我们正在开始探讨更深入的问题。 - luqui
1
你所追求的单子的单子状态必须依赖于一些纯代码属性,该属性不能由单子法则保证。其中一个示例是const x y == x(在单子世界中是const <$> x <*> y == x,这是不正确的)。也许这是一个开始的地方。 - luqui
3
ListT done right”来自于2007年。最初(有点无效的)ListT出现在2002年的GHC 5中。因此,我不确定甚至能称之为“相对较近”。 - dfeuer
4
如果T是一个Monad变换器并且存在一个Monad同构将T IdentityF联系起来(也就是说,一个标准的同构映射,它遵循绑定和返回操作),那么可以假设TF的一个Monad变换器。 - Daniel Wagner
显示剩余17条评论
1个回答

1

这不是一个答案,但是对于评论来说太大了。我们可以写:

{-# LANGUAGE GeneralizedNewtypeDeriving
  , DeriveFunctor #-}

import Control.Monad.Free

-- All the IO primops you could ever need
data IOF a = PutStrLn String a
           | GetLine (String -> a)
           deriving Functor

newtype MyIO a = MyIO {unMyIO :: Free IOF a}
  deriving (Functor, Applicative, Monad)

但是我们实际上可以将它做成一个单子变换器:

import Control.Monad.Trans.Free

newtype IOT m a = IOT {unIOT :: FreeT IOF m a}
  deriving (Functor, Applicative, Monad, MonadTrans)

因此,我并不认为即使是IO也被排除在外,尽管在这种情况下的同构不是“内部的”。

你似乎首先要从给定的monad构建一个自由monad。这将产生一个不同的monad。从monad F构建的自由monad当然有一个monad变换器,但它不会是F的monad变换器。 - winitzki
@winitzki,IOF不是一个单子。它是IO的基本函子。您可以使用FreeFreeT的Church版本来获得更精确的同构,但我认为如果您注意抽象屏障,这已经足够了。 - dfeuer
就像我之前说的,这并不是一个正确的答案,因为(与 IO 不同),并不是每个单子都是自由单子。 - dfeuer
如何定义IOF,什么是类型表达式 IOF a ,以使得Free IOF(即在IOF上构建的自由单子)等同于IO 单子?我认为,要么这是不可能的,或者 IOF 必须是一个不透明类型,在纯λ演算中无法定义,就像IO一样。在你给出的例子中,IOF a 被定义为等效于 Either (String, a) (String -> a) 的函子。基于此的自由单子肯定是一个单子,但它并不同于 IO 单子。 - winitzki
1
@winitzki,那种类型代表了一个具有更多构造函数的假设类型。我假设IOIOT被模块系统隐藏起来,因此它们只公开某些操作(不能一般性地进行检查)。实际上,Haskell 实现可以以类似的方式实现 IO。显然,在 Glasgow Haskell 中无法将 IO a -> MyIO a 进行映射,但这并不是一个非常有趣的事实。 - dfeuer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接