好的,所以编写器monad允许你把东西写到[通常]某种容器,并在最后收回容器。在大多数实现中,“容器”实际上可以是任何幺半群。
现在,还有一个“读者”monad。这个你可能认为,会提供双重操作 - 从某种容器逐步读取,一次一个项目。实际上,这不是通常的读者monad提供的功能。 (相反,它只是提供了对半全局常量的轻松访问。)
要实际写一个 双重的monad到通常的编写器monad,我们需要某种对monoid的双重结构。
答案 0 :(得分:28)
幺半群的双重是共生的。回想一下,monoid定义为(某些同构的)
class Monoid m where
create :: () -> m
combine :: (m,m) -> m
遵守这些法律
combine (create (),x) = x
combine (x,create ()) = x
combine (combine (x,y),z) = combine (x,combine (y,z))
因此
class Comonoid m where
delete :: m -> ()
split :: m -> (m,m)
需要一些标准操作
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)
这个类型类看起来很奇怪有一个原因。它有一个实例
instance Comonoid m where
split x = (x,x)
delete x = ()
在Haskell中,这是唯一的实例。我们可以将读者重新定义为作者的精确对偶,但由于只有一个comonoid实例,我们得到了与标准读者类型同构的东西。
所有类型都是comonoids是“笛卡尔闭合类别”中的类别“笛卡尔”。 “Monoidal Closed Categories”类似于CCC但没有这个属性,并且与子结构类型系统有关。线性逻辑的部分吸引力在于增加了对称性,这是一个例子。而具有子结构类型允许您定义具有更有趣属性的comonoids(支持资源管理等事情)。实际上,这提供了一个框架,用于理解C ++中拷贝构造函数和析构函数的作用(尽管由于指针的存在,C ++没有强制执行重要的属性)。
编辑:comonoids的读者
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
请注意,在上面的代码中,每个变量在绑定后只使用一次(因此这些变量都将使用线性类型)。 monad法律证明是微不足道的,只要求共生法律起作用。因此,Reader
确实是Writer
的双重身份。
答案 1 :(得分:12)
我不完全确定幺半群的双重性应该是什么,但是想到双重(可能是错误的)与某事物相反(仅仅基于Comonad是Monad的双重性,并且全部相同的操作,但相反的方式)。不是基于mappend
和mempty
,而是基于:
fold :: (Foldable f, Monoid m) => f m -> m
如果我们将f专门化为一个列表,我们得到:
fold :: Monoid m => [m] -> m
在我看来,特别是包含了所有的monoid类。
mempty == fold []
mappend x y == fold [x, y]
那么,我猜这个不同的monoid类的对偶是:
unfold :: (Comonoid m) => m -> [m]
这很像我在hackage here上看到的monoid阶乘阶级。
所以在此基础上,我认为你所描述的'读者'monad将是supply monad。供应monad实际上是值列表的状态转换器,因此在任何时候我们都可以选择从列表中提供项目。在这种情况下,列表将是unfold.supply monad的结果
我应该强调,我不是哈斯克尔专家,也不是专家理论家。但这就是你的描述让我想到的。
答案 2 :(得分:3)
供应基于State,这使得它对某些应用程序来说不是最理想的。例如,我们可能想要创建一个无限的提供值树(例如randoms):
tree :: (Something r) => Supply r (Tree r)
tree = Branch <$> supply <*> sequenceA [tree, tree]
但是由于Supply基于State,所以所有标签都是底部,除了树下最左边的路径。
你需要一些可分割的东西(比如@ PhillipJF的Comonoid
)。但如果你试图把它变成Monad就会出现问题:
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''
因为monad法律要求f >>= return = f
,所以这意味着r'' = r
定义中的(>>=)
..但是,monad法律也要求return x >>= f = f x
,所以{ {1}}也是。因此,r' = r
成为monad Supply
,因此您可以重新使用常规旧版split x = (x,x)
。
在Haskell中使用的很多monad都不是真正的monad - 即它们只满足达到某种等价关系的规律。例如。如果按照法律进行转换,许多非确定性monad将以不同的顺序给出结果。但是没关系,如果你只想知道是否特定元素出现在输出列表中,而不是其中,那么它仍然是monad。
如果你允许Reader
成为一个等价关系的monad,那么你可以得到非常重要的分裂。例如。 value-supply将构建可拆分实体,这些实体将以未指定的顺序从列表中发出唯一标签(使用Supply
魔法) - 因此,供应价值供应单元将是标签排列的单一元素。这就是许多应用程序所需要的。而且,事实上,有一个功能
unsafe*
它抽象了这个等价关系,给出了一个定义良好的纯接口,因为它允许你对标签做的唯一事情是看它们是否相等,如果你置换它们就不会改变。如果此runSupply :: (forall r. Eq r => Supply r a) -> a
是您在runSupply
上允许的唯一观察,那么Supply
上的唯一标签供应就是真正的单身。