答案 0 :(得分:117)
与以往一样,人们使用的术语并不完全一致。有各种各样的灵感来自单子 - 但严格来说 - 并不是完全的概念。术语"索引monad"用于表征一个这样的概念的术语中的一个(包括" monadish"和#34;参数化monad"(Atkey'它们的名称))。 (如果你感兴趣,另一个这样的概念是Katsumata"参数效应monad",由monoid索引,其中返回被中性索引并且绑定在其索引中累积。)
首先,让我们检查一下。
IxMonad (m :: state -> state -> * -> *)
即"计算的类型" (或"动作",如果你愿意,但我会坚持"计算"),看起来像
m before after value
其中before, after :: state
和value :: *
。我们的想法是捕获与具有一些可预测状态概念的外部系统安全交互的方法。计算类型告诉您状态必须是before
它运行的状态,它运行的状态after
以及(与*
上的常规monad一样)什么类型的value
计算产生。
通常的点点滴滴是*
- 明智的像monad和state
- 明智地喜欢玩多米诺骨牌。
ireturn :: a -> m i i a -- returning a pure value preserves state
ibind :: m i j a -> -- we can go from i to j and get an a, thence
(a -> m j k b) -- we can go from j to k and get a b, therefore
-> m i k b -- we can indeed go from i to k and get a b
" Kleisli arrow" (由此产生计算的函数)由此产生
a -> m i j b -- values a in, b out; state transition i to j
我们得到了一个作文
icomp :: IxMonad m => (b -> m j k c) -> (a -> m i j b) -> a -> m i k c
icomp f g = \ a -> ibind (g a) f
并且,与以往一样,法律确切地确保ireturn
和icomp
为我们提供了一个类别
ireturn `icomp` g = g
f `icomp` ireturn = f
(f `icomp` g) `icomp` h = f `icomp` (g `icomp` h)
或者,在喜剧中假冒C / Java /无论如何,
g(); skip = g()
skip; f() = f()
{h(); g()}; f() = h(); {g(); f()}
为什么要这么麻烦?建模"规则"互动例如,如果驱动器中没有DVD,则无法弹出DVD,如果已有DVD,则无法将DVD插入驱动器。所以
data DVDDrive :: Bool -> Bool -> * -> * where -- Bool is "drive full?"
DReturn :: a -> DVDDrive i i a
DInsert :: DVD -> -- you have a DVD
DVDDrive True k a -> -- you know how to continue full
DVDDrive False k a -- so you can insert from empty
DEject :: (DVD -> -- once you receive a DVD
DVDDrive False k a) -> -- you know how to continue empty
DVDDrive True k a -- so you can eject when full
instance IxMonad DVDDrive where -- put these methods where they need to go
ireturn = DReturn -- so this goes somewhere else
ibind (DReturn a) k = k a
ibind (DInsert dvd j) k = DInsert dvd (ibind j k)
ibind (DEject j) k = DEject j $ \ dvd -> ibind (j dvd) k
有了这个,我们就可以定义"原语"命令
dInsert :: DVD -> DVDDrive False True ()
dInsert dvd = DInsert dvd $ DReturn ()
dEject :: DVDrive True False DVD
dEject = DEject $ \ dvd -> DReturn dvd
其他人与ireturn
和ibind
汇编在一起。现在,我可以写(借用do
- 符号)
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dvd' <- dEject; dInsert dvd ; ireturn dvd'
但不是物理上不可能的
discSwap :: DVD -> DVDDrive True True DVD
discSwap dvd = do dInsert dvd; dEject -- ouch!
或者,可以直接定义一个基本命令
data DVDCommand :: Bool -> Bool -> * -> * where
InsertC :: DVD -> DVDCommand False True ()
EjectC :: DVDCommand True False DVD
然后实例化通用模板
data CommandIxMonad :: (state -> state -> * -> *) ->
state -> state -> * -> * where
CReturn :: a -> CommandIxMonad c i i a
(:?) :: c i j a -> (a -> CommandIxMonad c j k b) ->
CommandIxMonad c i k b
instance IxMonad (CommandIxMonad c) where
ireturn = CReturn
ibind (CReturn a) k = k a
ibind (c :? j) k = c :? \ a -> ibind (j a) k
实际上,我们已经说过原始的Kleisli箭头是什么(什么是#34;多米诺&#34;是),然后建立了一个合适的概念&#34;计算序列&#34;在他们身上。
请注意,对于每个已编入索引的monad m
,&#34;无更改对角线&#34; m i i
是一个monad,但一般情况下,m i j
不是。此外,值没有被索引,但是计算被索引,因此索引monad不仅仅是monad为其他类别实例化的通常想法。
现在,再看一下Kleisli箭头的类型
a -> m i j b
我们知道我们必须处于状态i
才能开始,我们预测任何延续都将从州j
开始。我们对这个系统了解很多!这不是一个危险的操作!当我们将DVD放入驱动器时,它会进入!在每个命令之后,DVD驱动器对状态没有任何发言权。
但在与世界互动时,一般情况并非如此。有时你可能需要放弃一些控制,让世界做自己喜欢的事。例如,如果您是服务器,则可以为您的客户提供一个选择,您的会话状态将取决于他们选择的内容。服务器&#34;提供选择&#34;操作不确定结果状态,但服务器应该能够继续进行。它不是&#34;原始命令&#34;从上述意义上讲,索引monad并不是模拟不可预测的场景的好工具。
什么是更好的工具?
type f :-> g = forall state. f state -> g state
class MonadIx (m :: (state -> *) -> (state -> *)) where
returnIx :: x :-> m x
flipBindIx :: (a :-> m b) -> (m a :-> m b) -- tidier than bindIx
可怕的饼干?不是真的,原因有两个。一,它看起来更像monad是什么,因为 是一个monad,但超过(state -> *)
而不是*
。第二,如果你看一下Kleisli箭头的类型,
a :-> m b = forall state. a state -> m b state
您可以使用前提条件 a
和后置条件b
获得计算类型,就像Good Old Hoare Logic一样。程序逻辑中的断言已经花了不到半个世纪的时间来穿越Curry-Howard的对应并成为Haskell类型。 returnIx
的类型表示&#34;你可以实现任何后置条件,只是不做任何事情&#34;,这是&#34;跳过&#34;的Hoare Logic规则。相应的合成是&#34 ;;&#34;。
通过查看bindIx
的类型,完成所有量词,让我们完成。
bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i
这些forall
具有相反的极性。我们选择初始状态i
,并且计算可以从i
开始,具有后置条件a
。世界选择它喜欢的任何中间状态j
,但它必须向我们提供后置条件b
成立的证据,并且从任何这样的状态,我们可以继续使b
成立。因此,按顺序,我们可以从状态b
获得条件i
。通过释放&#34;&#34;&#34;&#34;状态,我们可以建模不可预测的计算。
IxMonad
和MonadIx
都很有用。交互式计算的模型有效性分别相对于改变状态,可预测和不可预测。可预测性在获得它时是有价值的,但不可预测性有时是生活中的事实。那么,希望这个答案可以说明索引monad是什么,预测它们何时开始有用以及何时停止。
答案 1 :(得分:44)
至少有三种方法可以定义我知道的索引monad。
我将这些选项称为索引monadsàlaX ,其中X的范围超过了计算机科学家Bob Atkey,Conor McBride和Dominic Orchard,因为这是我倾向于考虑它们的方式。这些结构的一部分通过类别理论具有更长的更辉煌的历史和更好的解释,但我首先了解到它们与这些名称相关联,并且我试图保持这个答案而不是太深奥。
Bob Atkey的索引monad风格是使用2个额外的参数来处理monad的索引。
通过这种方式,您可以获得人们在其他答案中所抛出的定义:
class IMonad m where
ireturn :: a -> m i i a
ibind :: m i j a -> (a -> m j k b) -> m i k b
我们也可以定义索引的comonadsàlaAtkey。实际上我从那些in the lens
codebase获得了很多里程。
下一种形式的索引monad是Conor McBride在他的论文"Kleisli Arrows of Outrageous Fortune"中的定义。相反,他使用单个参数作为索引。这使得索引monad定义具有相当聪明的形状。
如果我们使用参数化定义自然变换如下
type a ~> b = forall i. a i -> b i
然后我们可以将McBride的定义写为
class IMonad m where
ireturn :: a ~> m a
ibind :: (a ~> m b) -> (m a ~> m b)
这与Atkey的感觉完全不同,但感觉更像是普通的Monad,而不是在(m :: * -> *)
上构建monad,我们在(m :: (k -> *) -> (k -> *)
上构建它。
有趣的是,你可以通过使用聪明的数据类型从McBride's恢复Atkey的索引monad风格,McBride以他独特的风格选择说你应该读作“关键”。
data (:=) :: a i j where
V :: a -> (a := i) i
现在你可以解决这个问题
ireturn :: IMonad m => (a := j) ~> m (a := j)
扩展为
ireturn :: IMonad m => (a := j) i -> m (a := j) i
只能在j = i时调用,然后仔细阅读ibind
可以让你回到与Atkey ibind
相同的位置。您需要传递这些(:=)数据结构,但它们可以恢复Atkey演示的强大功能。
另一方面,Atkey演示文稿不足以恢复McBride版本的所有用途。权力已得到严格控制。
另一件好事是McBride的索引monad显然是monad,它只是一个不同的functor类别的monad。它适用于从(k -> *)
到(k -> *)
的仿函数类别的endofunctors,而不是从*
到*
的仿函数类别。
一个有趣的练习是弄清楚如何为索引的 comonads 进行McBride to Atkey转换。我个人在McBride的论文中使用数据类型'At'作为“关键”构造。我实际上走到了ICFP 2013的鲍勃·阿斯基(Bob Atkey)那里,并提到我把他翻过来让他变成了“外套”。他似乎明显不安。这条线在我脑海中发挥得更好。 =)
最后,第三个被广泛引用的“索引monad”名称的索赔人归功于Dominic Orchard,他在那里使用类型级别的monoid来粉碎索引。我只是链接到这个话题,而不是详细介绍构造:
答案 2 :(得分:23)
作为一个简单的场景,假设你有一个状态monad。状态类型是一个复杂的大型,但所有这些状态可以分为两组:红色和蓝色状态。只有当前状态为蓝色状态时,此monad中的某些操作才有意义。其中,有些会使状态保持蓝色(blueToBlue
),而其他则会使状态变为红色(blueToRed
)。在常规monad中,我们可以写
blueToRed :: State S ()
blueToBlue :: State S ()
foo :: State S ()
foo = do blueToRed
blueToBlue
触发运行时错误,因为第二个操作需要蓝色状态。我们想静态地防止这种情况。索引monad实现了这个目标:
data Red
data Blue
-- assume a new indexed State monad
blueToRed :: State S Blue Red ()
blueToBlue :: State S Blue Blue ()
foo :: State S ?? ?? ()
foo = blueToRed `ibind` \_ ->
blueToBlue -- type error
触发类型错误,因为blueToRed
(Red
)的第二个索引与blueToBlue
(Blue
)的第一个索引不同。
作为另一个例子,对于索引monad,你可以允许状态monad改变其状态的类型,例如你可能有
data State old new a = State (old -> (new, a))
您可以使用上面的内容来构建一个静态类型的异构堆栈。操作将具有类型
push :: a -> State old (a,old) ()
pop :: State (a,new) new a
作为另一个例子,假设您想要一个不受限制的IO monad 允许文件访问。你可以使用例如。
openFile :: IO any FilesAccessed ()
newIORef :: a -> IO any any (IORef a)
-- no operation of type :: IO any NoAccess _
通过这种方式,静态保证类型为IO ... NoAccess ()
的动作不受文件访问。相反,类型IO ... FilesAccessed ()
的操作可以访问文件。拥有索引monad意味着您不必为受限制的IO构建单独的类型,这需要复制两种IO类型中的每个非文件相关的函数。
答案 3 :(得分:17)
索引monad不是一个特定的monad,例如,状态monad,而是一种带有额外类型参数的monad概念的泛化。
“标准”monadic值的类型为Monad m => m a
,而索引monad中的值为IndexedMonad m => m i j a
,其中i
和j
为索引类型,因此{{ 1}}是monadic计算开始时的索引类型,计算结束时是i
。在某种程度上,您可以将j
视为一种输入类型,并将i
视为输出类型。
以j
为例,有状态计算State
在整个计算过程中保持类型State s a
的状态,并返回类型s
的结果。索引版本a
是一种有状态计算,其中状态可以在计算期间变为不同类型。初始状态的类型为IndexedState i j a
和state,计算结束的类型为i
。
很少需要在普通monad上使用索引monad,但在某些情况下可以使用它来编码更严格的静态保证。
答案 4 :(得分:5)
了解索引如何在依赖类型中使用(例如在agda中)可能很重要。这可以解释索引如何帮助一般,然后将此体验转换为monad。
索引允许在特定类型实例之间建立关系。然后你可以推理一些值来确定这种关系是否成立。
例如(在agda中)您可以指定某些自然数与_<_
相关,并且类型指出它们是哪些数字。然后你可以要求给某个函数一个m < n
的见证,因为只有这个函数才能正常工作 - 并且没有提供这样的见证,程序将无法编译。
作为另一个例子,如果有足够的毅力和编译器支持您选择的语言,您可以编码该函数假定某个列表已排序。
索引monad允许对某些依赖类型系统进行编码,以更精确地管理副作用。