Haskell ";“读者”;单子
好的,编写器monad允许您将内容写入某种容器,并在最后将该容器取回。在大多数实现中,“容器”实际上可以是任何幺半群 现在,还有一个“阅读器”单子。您可能会认为,这将提供双重操作—增量地从某种容器中读取,一次读取一个项目。事实上,这不是通常的阅读器monad提供的功能。(相反,它只提供了对半全局常数的简单访问。) 要真正写出一个与通常的编写器monad是对偶的monad,我们需要某种与monoid是对偶的结构Haskell ";“读者”;单子,haskell,monads,Haskell,Monads,好的,编写器monad允许您将内容写入某种容器,并在最后将该容器取回。在大多数实现中,“容器”实际上可以是任何幺半群 现在,还有一个“阅读器”单子。您可能会认为,这将提供双重操作—增量地从某种容器中读取,一次读取一个项目。事实上,这不是通常的阅读器monad提供的功能。(相反,它只提供了对半全局常数的简单访问。) 要真正写出一个与通常的编写器monad是对偶的monad,我们需要某种与monoid是对偶的结构 有人知道这种双重结构可能是什么吗 有人写过这个单子吗?有一个有名的名字吗 我不完全确定
我不完全确定幺半群的对偶应该是什么,但我认为对偶(可能是错误的)是某物的对立面(仅仅基于一个Comonad是一个Monad的对偶,并且具有所有相同的操作,但方向相反)。与其基于
mappend
和mempty
我不如基于:
fold :: (Foldable f, Monoid m) => f m -> m
如果我们在这里专门列出f,我们会得到:
fold :: Monoid m => [m] -> m
在我看来,这似乎包含了所有的幺半群类,尤其是
mempty == fold []
mappend x y == fold [x, y]
那么,我猜这个不同幺半群类的对偶是:
unfold :: (Comonoid m) => m -> [m]
这很像我在hackage上看到的幺半群阶乘类
所以在这个基础上,我认为你描述的“读者”单子应该是一个。supply monad实际上是一个值列表的状态转换器,因此在任何时候我们都可以选择从列表中提供一个项。在这种情况下,列表将是unfold.supply monad的结果
我应该强调,我不是哈斯凯尔的专家,也不是一个专家理论家。但这就是你的描述让我想到的。幺半群的对偶是一个comonoid。回想一下,幺半群被定义为(同构的东西) 根据这些法律
combine (create (),x) = x
combine (x,create ()) = x
combine (combine (x,y),z) = combine (x,combine (y,z))
因此
需要一些标准操作
first :: (a -> b) -> (a,c) -> (b,c)
second :: (c -> d) -> (a,c) -> (a,d)
idL :: ((),x) -> x
idR :: (x,()) -> x
assoc :: ((x,y),z) -> (x,(y,z))
有这样的法律
idL $ first delete $ (split x) = x
idR $ second delete $ (split x) = x
assoc $ first split (split x) = second split (split x)
这个typeclass看起来很奇怪是有原因的。它有一个例子
instance Comonoid m where
split x = (x,x)
delete x = ()
在Haskell中,这是唯一的例子。我们可以将reader改写为writer的完全对偶,但因为comonoid只有一个实例,所以我们得到了与标准reader类型同构的东西
使所有类型都是共面体是“笛卡尔闭范畴”中的“笛卡尔”范畴的原因。“单倍体闭范畴”类似于CCC,但没有此属性,并且与子结构类型系统相关。线性逻辑的吸引力部分在于它的对称性增强,这就是一个例子。然而,拥有子结构类型允许您定义具有更有趣属性的comonoid(支持资源管理之类的东西)。事实上,这为理解C++中的复制构造函数和析构函数的作用提供了框架(尽管C++由于指针的存在而没有强制执行重要的属性)。
编辑:来自科莫类的读取器
newtype Reader r x = Reader {runReader :: r -> x}
forget :: Comonoid m => (m,a) -> a
forget = idL . first delete
instance Comonoid r => Monad (Reader r) where
return x = Reader $ \r -> forget (r,x)
m >>= f = \r -> let (r1,r2) = split r in runReader (f (runReader m r1)) r2
ask :: Comonoid r => Reader r r
ask = Reader id
请注意,在上面的代码中,每个变量在绑定后只使用一次(因此这些变量都将使用线性类型)。单子定律的证明是琐碎的,只需要科摩罗定律就行了。因此,
读卡器
实际上与写卡器
是双重的供应是基于状态的,这使得它对于某些应用程序来说是次优的。例如,我们可能希望生成一个由提供的值(例如随机数)组成的无限树:
因为单子定律要求f>=return=f
,所以这意味着r'=r
在(>>=)
的定义中。。但是,单子定律也要求返回x>=f=fx
,因此r'=r
。因此,Supply
要成为一个单子,splitx=(x,x)
,这样您就可以重新获得常规的阅读器
Haskell中使用的许多单子都不是真正的单子——也就是说,它们只满足一些等价关系的定律。例如,如果你按照定律变换,许多不确定的单子将以不同的顺序给出结果。但这没关系,如果您只是想知道某个特定元素是否出现在输出列表中,而不是在哪里,那么这仍然是monad
如果您允许Supply
成为一个单子,直到某个等价关系,那么您可以得到非平凡的拆分。例如,将构造可拆分实体,该实体将以未指定的顺序从列表中分配唯一的标签(使用不安全*
magic)——因此,值供应的供应单元组将是标签排列的单元组。这是许多应用程序所需要的全部。事实上,有一个函数
runSupply :: (forall r. Eq r => Supply r a) -> a
它抽象了这个等价关系,给出了一个定义良好的纯接口,因为它允许你对标签做的唯一事情就是看它们是否相等,如果你排列它们,这不会改变。如果这个runSupply
是您在Supply
上允许的唯一观察,那么在一系列独特标签上的Supply
就是一个真正的单子。编剧单子的双重身份不是一个共同编剧吗?(只是开玩笑,我真的不理解你的想象。也许是一个状态
消费一个流?@dainelfischer这听起来很疯狂,甚至可能是正确的……第二个问题让我想到了人们提出的无限流处理库(迭代器、管道、导管等)“阅读器”monad通常被称为环境monad(我认为后一个名字可能实际上比前一个名字早——早期参考文献见David Espinosa在90年代中期的论文),因此期望读者是作家的“双重”可能期望过高。我不确定科摩诺人的东西,但是供应的单子看起来确实很像我所想的。这肯定不是唯一一个可能的例子吗?怎么样:实例Comonoid[a],其中{delete=const();s
newtype Reader r x = Reader {runReader :: r -> x}
forget :: Comonoid m => (m,a) -> a
forget = idL . first delete
instance Comonoid r => Monad (Reader r) where
return x = Reader $ \r -> forget (r,x)
m >>= f = \r -> let (r1,r2) = split r in runReader (f (runReader m r1)) r2
ask :: Comonoid r => Reader r r
ask = Reader id
tree :: (Something r) => Supply r (Tree r)
tree = Branch <$> supply <*> sequenceA [tree, tree]
newtype Supply r a = Supply { runSupply :: r -> a }
instance (Splittable r) => Monad (Supply r) where
return = Supply . const
Supply m >>= f = Supply $ \r ->
let (r',r'') = split r in
runSupply (f (m r')) r''
runSupply :: (forall r. Eq r => Supply r a) -> a