什么是索引单子?


98

什么是索引式monad以及此monad的动机?

我已经读过它有助于跟踪副作用。但是类型签名和文档并没有带我到任何地方。

它将如何帮助跟踪副作用(或任何其他有效的例子)的例子是什么?

Answers:


123

与以往一样,人们使用的术语并不完全一致。有各种各样的受单子启发,但严格说来,并不是很多的想法。术语“索引单子”是用于表征一个这样的概念的术语(包括“单子”和“参数化单子”(Atkey的名称))之一。(如果您感兴趣的话,另一种这样的概念是Katsumata的“参数效应单子”,它由一个等分体索引,其中return被中性地索引并在其索引中累积)。

首先,让我们检查种类。

IxMonad (m :: state -> state -> * -> *)

也就是说,“计算”(或“动作”,如果您愿意,但我会坚持使用“计算”​​)的类型看起来像

m before after value

在哪里before, after :: statevalue :: *。想法是捕获与具有某些可预测状态概念的外部系统安全交互的方式。计算的类型告诉您计算必须before运行的状态,运行的状态after以及(例如,对于常规monad而言*value计算产生哪种类型的s。

通常的点点*滴滴-像单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

而且,法律一如既往地确保了这一点,ireturnicomp给了我们一个类别

      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()
{g(); h()}; 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

从其它的组装ireturnibind。现在,我可以写(借用- 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箭头是什么(一个“多米诺”是什么),然后在其上建立了一个合适的“计算序列”概念。

请注意,对于每个索引的monad m,“无变化对角线” m i i是monad,但通常m i j不是。而且,不对值建立索引,但对计算建立索引,因此建立索引的monad不仅仅是为其他类别实例化monad的通常想法。

现在,再次查看Kleisli箭头的类型

a -> m i j b

我们知道我们必须处于状态i才能开始,并且我们预测任何延续都将从状态开始j。我们对该系统了解很多!这不是冒险的操作!当我们将DVD放入驱动器时,它会进入!dvd驱动器在每个命令后的状态都没有任何发言权。

但这通常与世界互动时并非如此。有时您可能需要放弃一些控制权,让世界做自己喜欢的事。例如,如果您是服务器,则可以为客户端提供选择,并且会话状态将取决于他们的选择。服务器的“提供选择”操作不能确定结果状态,但是服务器仍然应该能够继续操作。从上述意义上说,这不是“原始命令”,因此索引的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说的类型是“只要不做任何事情就可以达到任何保留的后置条件”,这是“跳过”的Hoare Logic规则。相应的组成是“;”的Hoare逻辑规则。

让我们来看一下的类型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从state 达到条件i。通过释放对“之后”状态的控制,我们可以对不可预测的计算进行建模。

这两个IxMonadMonadIx是有用的。两种模型分别针对可预测和不可预测的状态变化对交互式计算的有效性进行了建模。可预测性在您可以得到时很有价值,但是不可预测性有时是生活中的事实。希望这个答案能给出一些指示什么是索引的monad,并预测它们何时开始有用以及何时停止。


1
如何将True/ False值作为类型参数传递给DVDDrive?那是一些扩展名,还是布尔型实际上在这里输入?
Bergi

8
@Bergi布尔值已被“提升”到类型级别。在Haskell中,可以使用DataKinds扩展名和依存类型的语言来实现……好吧,这就是全部。
J. Abrahamson

MonadIx可以在示例上扩展一点吗?从理论上讲是更好还是在实际应用中更好?
Christian Conkle

2
@ChristianConkle我意识到这不是很有帮助。但是您提出了另一个真正的问题。就本地而言,当我说MonadIx“更好”时,我的意思是在对具有不可预测环境的交互进行建模的上下文中。就像如果您的DVD驱动器被允许吐出DVD一样,当您尝试插入它们时就不一样。在某些实际情况下,这种行为是很糟糕的。其他人则具有更高的可预测性(这意味着您可以说任何连续性在什么状态下开始,而不是操作不会失败),在这种情况下,IxMonad更易于使用。
Pigworker

1
当您“借用”答案中的do表示法时,可以说它实际上是RebindableSyntax扩展名有效的语法。像前面提到的那样,提及其他必需的扩展将是不错的选择DataKinds
千兆字节

46

至少有三种方法可以定义索引的monad。

我将这些选项称为带索引的 monadàX,其中X覆盖计算机科学家Bob Atkey,Conor McBride和Dominic Orchard,因为这就是我倾向于想到它们的方式。通过类别理论,这些结构的某些部分具有更长久的辉煌历史和更好的解释,但是我首先了解到它们与这些名称相关联,并且我试图避免使这个答案变得深奥。

Atkey

鲍勃·阿特基(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

我们还可以定义索引的符号Atatkey。实际上,我lens代码库中学到了很多东西。

麦克布莱德

被索引的monad的下一种形式是Conor McBride从他的论文《离奇的财富Kleisli Arrows》中得到的定义。他改为使用单个参数作为索引。这使得索引的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,而不是在on上(m :: * -> *)构建monad (m :: (k -> *) -> (k -> *)

有趣的是,您实际上可以使用聪明的数据类型从McBride的索引中恢复Atkey的索引单子样式,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,它只是函子类别上的monad。它endofunctors工作过的函子类别从(k -> *)(k -> *),而不是仿函数从类别**

一个有趣的练习是弄清楚如何对索引的共体进行McBride到Atkey的转换。我个人将数据类型“ At”用于McBride论文中的“关键”构造。在ICFP 2013上,我实际上走到了鲍勃·阿特基(Bob Atkey)的身边,并提到我把他变成了“外套”。他似乎很不安。这条线在我的脑海中表现得更好。=)

果园

最后,第三个被称为“索引单子”的人是多米尼克·乌节(Dominic Orchard)的名字,这是一个鲜为人知的说法,他在那里使用类型级别的monoid将索引粉碎在一起。除了简单地讨论构造细节之外,我仅链接到此讨论:

https://github.com/dorchard/effect-monad/blob/master/docs/ixmonad-fita14.pdf


1
我对Orchard的monad等同于Atkey的理解是正确的吗,因为我们可以通过采用内同态等式从前者转到后者,而在状态转换中通过CPS编码的等式附加项来倒退?
安德拉斯·科瓦克斯

这对我来说听起来很合理。
爱德华KMETT

话虽如此,根据他在ICFP 2013上对我说的话,我相信Orchard希望他的类型家族表现得像一个真正的类人猩猩,而不是像某些箭头无法连接的任意类别,所以这个故事可能还有更多除此之外,Atkey的构造使您可以轻松地限制某些Kleisli动作与其他动作的连接-在很多方面,这都是McBride版本的重点。
爱德华·KMETT

2
扩展“仔细阅读ibind”:介绍类型别名Atkey m i j a = m (a := j) i。将其用作mAtkey的定义将恢复我们搜索的两个签名:ireturnAtkin :: a -> m (a := i) iibindAtkin :: m (a := j) i -> (a -> m (b := k) j) -> m (b := k) i。第一个是通过组成获得的:ireturn . V。第二个方法是(1)forall j. (a := j) j -> m (b := k) j通过模式匹配构建函数,然后将恢复的函数传递a给的第二个参数ibindAtkin
WorldSEnder

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

被触发A型错误,因为所述第二折射率blueToRedRed)从所述第一折射率不同blueToBlueBlue)。

再举一个例子,使用索引的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类型中都复制每个与文件无关的功能。


18

有索引的monad不是特定的monad,例如状态monad,而是对monad概念的一种泛型,带有额外的类型参数。

而“标准”单子数值的类型Monad m => m a为已索引的单子数值,IndexedMonad m => m i j a其中的值将为,i并且j为索引类型,因此这i是在单子运算开始时和计算j结束时的索引类型。在某种程度上,您可以将其i视为一种输入类型和j一种输出类型。

使用State作为一个例子,有状态计算State s a维持型的状态s在整个计算和返回类型的结果a。索引版本IndexedState i j a是一种有状态计算,其中状态可以在计算过程中更改为其他类型。初始状态具有类型i和状态,计算结束具有类型j

很少需要在普通monad上使用索引monad,但是在某些情况下可以使用它来编码更严格的静态保证。


5

重要的是要看看如何在依赖类型中使用索引(例如,在agda中)。这可以解释索引编制通常有何帮助,然后将这种经验转化为monad。

索引允许在特定类型的实例之间建立关系。然后,您可以推断出一些值来确定该关系是否成立。

例如(在agda中),您可以指定一些自然数与关联_<_,并且类型告诉它们它们是哪个数。然后,您可以要求给某个函数提供一个见证人m < n,因为只有这样该功能才能正常工作-如果不提供此类见证人,则程序将无法编译。

再举一个例子,如果您对所选语言有足够的毅力和编译器支持,则可以对函数进行编码,使其假定某个列表已排序。

带索引的monad允许对某些依赖类型的系统进行编码,以更精确地管理副作用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.