为monad定义>> =的行为

时间:2014-05-05 13:07:03

标签: haskell monads

我在大学的Haskell学习monad,这是我们给出的练习:

给出类型

data Expr a = Var a | Val Int | Add (Expr a) (Expr a)

从类型a的变量构建的表达式,通过完成以下声明显示此类型是monadic:

instance Monad Expr where
--  return         :: a -> Expr a
    return          = ...

--  (>>=)          :: Expr a -> (a -> Expr b) -> Expr b
    (Var a)   >>= f = ...
    (Val n)   >>= f = ...
    (Add x y) >>= f = ...

到目前为止,我已经逻辑设法提出:

return        = Var

(Var a) >>= f = f a
(Val n) >>= f = Val n

但是如何定义(Add x y) >>= f

2 个答案:

答案 0 :(得分:4)

这是一个打字俄罗斯方块的游戏。让我们考虑前两个(正确)分支的类型。

a :: a
f :: a -> Expr b
----------------
Expr b

我们读到这个"考虑到线路上方的所有内容,我们必须生成下面第34行所列类型的东西。在这种情况下,答案显而易见:我们只是将f应用于a

a :: a
f :: a -> Expr b
----------------
f a :: Expr b

对于Val n,我们可以重复这个过程

n :: Int
f :: a -> Expr b
----------------
Expr b

起初这似乎是不可能的,但我们已经省略了一些事情并且排在第34行之上。实际上是可用的。重要的是,有这样的:

n :: Int
f :: a -> Expr b
Val :: forall x . Int -> Expr x
--------------------
Expr b

由于x不是特定的(受forall约束),因此可以与b统一。

n :: Int
f :: a -> Expr b
Val :: forall x . Int -> Expr x
--------------------
Val n :: Expr b

对于最后一种情况,我们有

x   :: Expr a
y   :: Expr a
f   :: a -> Expr b
Add :: forall x . Expr x -> Expr x -> Expr x
--------------------------------------------
Expr b

同样,这似乎是不可能的。我们可以使用Addx重新组合y,但这只是给我们一个Expr a类型而不是Expr b。我们无法应用f,因为它需要a而不是Expr a

所以诀窍在于,对于递归数据类型,你几乎肯定会对函数使用递归定义......所以让我们从环境中引入另一个东西。

x     :: Expr a
y     :: Expr a
f     :: a -> Expr b
Add   :: forall x . Expr x -> Expr x -> Expr x
(>>=) :: forall y z . Expr y -> (y -> Expr z) -> Expr z
-------------------------------------------------------
Expr b

同样,由于forall,我们可以在(>>=)类型上使用Expr,无论其变量是什么。我们几乎只有一种方法可以继续前进:我们唯一可以成为(>>=)的第二个参数的值是f

x       :: Expr a
y       :: Expr a
Add     :: forall x . Expr x -> Expr x -> Expr x
(>>= f) :: Expr a -> (a -> Expr b) -> Expr b
-------------------------------------------------------
Expr b

现在我们可以在x的左侧应用y(>>= f)来获取类型为Expr b的值。不幸的是,这两个都是错误的。确保这一点的一种方法是,对于像(>>=)这样的高度通用的函数,我们几乎不会丢弃信息 - 每个参数都应该非常简单地使用。

幸运的是,如果我们有两个 Expr b,我们可以使用Add来合并它们:

x       :: Expr a
y       :: Expr a
Add     :: Expr b -> Expr b -> Expr b
(>>= f) :: Expr a -> (a -> Expr b) -> Expr b
-------------------------------------------------------
Expr b

现在我们有办法同时使用xy非正式

x       :: Expr a
y       :: Expr a
Add     :: Expr b -> Expr b -> Expr b
(>>= f) :: Expr a -> (a -> Expr b) -> Expr b
-------------------------------------------------------
Add (x >>= f) (y >>= f) :: Expr b

注意我们保持xy的顺序相同。


所以这种类型 - 俄罗斯方块游戏虽然有点啰嗦但却用一些原则证明:

  1. 递归类型的定义往往需要递归
  2. 一般函数的定义将尽可能使用每个参数
  3. 我们可以得到如何几乎完全机械地定义(>>=)的答案。一个好的定义应该感到满意,就像字面上没有别的选择一样。

    instance Monad Expr where
      Var a >>= f = f a
      Val n >>= _ = Val n
      Add x y >>= f = Add (x >>= f) (y >>= f)
      ...
    

    我们可以将此定义解释为"反应" f以及Var存在a的引导节点。如果不存在a,那么我们可以自由地做任何事情。如果存在递归Expr,那么我们只需将呼叫推送到(>>= f)下来"并重建递归类型。

答案 1 :(得分:3)

在这种情况下,我们可以通过检查类型来“跟随我们的鼻子”:当给定Add x y时,我们有两个Expr a类型的东西(x和{{1} })我们正在尝试输出y类型的东西。一种选择是完全忽略Expr bx并输出类似y的内容(但这不是很有趣,可能违反了Monad法则。)

因此,假设我们希望同时使用Val 0x,那么第一步就是将这两个y转换为Expr a:我们只有一个选择此处,因为Expr ba之间只有一个关系,函数b

现在,要使用这两条信息,我们可以考虑寻找类型为f :: a -> Expr b的函数,即需要我们的表达式(Expr a -> (a -> Expr b) -> Expr b)和函数({{ 1}})并给我们一个Expr a

好像很熟悉?

它应该:它正是我们正在使用的,绑定运算符a -> Expr b。通过使用此功能,我们现在有两个Expr b>>=Expr b

因此,最后一步是使用这两件事给出结果:一个(x >>= f) ......对此也有一个选择。