我在大学的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
?
答案 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
同样,这似乎是不可能的。我们可以使用Add
和x
重新组合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
现在我们有办法同时使用x
和y
非正式
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
注意我们保持x
和y
的顺序相同。
所以这种类型 - 俄罗斯方块游戏虽然有点啰嗦但却用一些原则证明:
我们可以得到如何几乎完全机械地定义(>>=)
的答案。一个好的定义应该感到满意,就像字面上没有别的选择一样。
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 b
和x
并输出类似y
的内容(但这不是很有趣,可能违反了Monad法则。)
因此,假设我们希望同时使用Val 0
和x
,那么第一步就是将这两个y
转换为Expr a
:我们只有一个选择此处,因为Expr b
和a
之间只有一个关系,函数b
。
现在,要使用这两条信息,我们可以考虑寻找类型为f :: a -> Expr b
的函数,即需要我们的表达式(Expr a -> (a -> Expr b) -> Expr b
)和函数({{ 1}})并给我们一个Expr a
。
它应该:它正是我们正在使用的,绑定运算符a -> Expr b
。通过使用此功能,我们现在有两个Expr b
:>>=
和Expr b
。
因此,最后一步是使用这两件事给出结果:一个(x >>= f)
......对此也有一个选择。