Haskell 什么是索引单子?

Haskell 什么是索引单子?,haskell,monads,Haskell,Monads,这个单子的动机是什么 我已经读到它有助于跟踪副作用。但是类型签名和文档并没有把我带到任何地方 什么样的例子可以帮助跟踪副作用(或任何其他有效的例子)?作为一个简单的场景,假设您有一个状态单子。状态类型是一种复杂的大型状态,但所有这些状态都可以划分为两组:红色和蓝色状态。只有当前状态为蓝色状态时,此monad中的某些操作才有意义。其中,一些将使状态保持蓝色(blueToBlue),而另一些将使状态变为红色(blueToRed)。在一个普通的单子里,我们可以写 blueToRed :: State

这个单子的动机是什么

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


什么样的例子可以帮助跟踪副作用(或任何其他有效的例子)?

作为一个简单的场景,假设您有一个状态单子。状态类型是一种复杂的大型状态,但所有这些状态都可以划分为两组:红色和蓝色状态。只有当前状态为蓝色状态时,此monad中的某些操作才有意义。其中,一些将使状态保持蓝色(
blueToBlue
),而另一些将使状态变为红色(
blueToRed
)。在一个普通的单子里,我们可以写

blueToRed  :: State S ()
blueToBlue :: State S ()

foo :: State S ()
foo = do blueToRed
         blueToBlue
触发运行时错误,因为第二个操作需要蓝色状态。我们希望静态地防止这种情况。索引单子实现了这一目标:

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
)的第一个索引不同

另一个例子是,使用索引单子,您可以允许状态单子更改其状态的类型,例如,您可以

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类型中复制每个与文件无关的函数。

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

鉴于“标准”一元值的类型为
Monad m=>ma
索引单元中的值为
IndexedMonad m=>mi j a
,其中
i
j
是索引类型,因此
i
是一元计算开始时的索引类型,而
j
是计算结束时的索引类型。在某种程度上,您可以将
i
视为一种输入类型,将
j
视为输出类型

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


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

一如既往,人们使用的术语并不完全一致。有各种各样的灵感来自单子,但严格地说是不完全的概念。术语“索引单子”是用于描述一个这样的概念的术语中的一个(包括“单子”和“参数化单子”(Atkey的名字)。(如果你感兴趣的话,另一个这样的概念是Katsumata的“参数效应单子”(parametriceffect monad),它由一个幺半群索引,其中return是中性索引,bind在其索引中累加。)

首先,让我们检查一下种类

IxMonad (m :: state -> state -> * -> *)
也就是说,“计算”(或者“动作”,如果你愿意的话,但我坚持使用“计算”)的类型

m before after value
其中
之前、之后::状态
值::*
。其思想是获取与具有某种可预测状态概念的外部系统安全交互的方法。计算的类型告诉您在运行之前必须是什么状态,在运行之后将是什么状态,以及(就像在*上使用常规单子一样)计算产生什么类型的值

通常的比特和碎片是
*
-像单子一样明智,而
状态
-像玩多米诺骨牌一样明智

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箭头”(产生计算的函数)的概念

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()
{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
有了它,我们可以定义“基本”命令

从中可以使用
ireturn
ibind
组装其他文件。现在,我可以写了(借用
do
-notation)

或者,可以直接定义自己的基本命令

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箭头是什么(一个“domino”是什么),然后在它们上面建立了一个合适的“计算序列”概念

请注意,对于每个索引的monad
m
,“无变化对角线”
mi
是一个monad,但通常,
mi j
不是。此外,值没有索引,但计算是索引的,因此索引单子并不仅仅是为其他类别实例化单子的通常想法

现在,再看看克莱斯利箭头的类型

a -> m i j b
我们知道我们必须处于状态
i
才能开始,并且我们预测任何延续都将从状态
j
开始。我们对这个系统了解很多!这不是一个冒险的行动!当我们把dvd放进光驱时,它就进去了!dvd驱动器在每个命令后都无法决定状态

但一般来说,当与世界互动时,情况并非如此
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
a -> m i j b
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
a :-> m b   =   forall state. a state -> m b state
bindIx :: forall i. m a i -> (forall j. a j -> m b j) -> m b i
class IMonad m where
  ireturn  ::  a -> m i i a
  ibind    ::  m i j a -> (a -> m j k b) -> m i k b
type a ~> b = forall i. a i -> b i 
class IMonad m where
  ireturn :: a ~> m a
  ibind :: (a ~> m b) -> (m a ~> m b)
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