函数的类型类实例

时间:2017-04-12 20:47:03

标签: function haskell monads functor applicative

我刚刚意识到,函数有Monad,Functor和Applicative的实例。

当我看到一些我没有得到的类型类实例时,我通常会做的是编写一个类型很好的表达式并查看它返回的内容:

有人可以解释这些情况吗?你通常会听到List和Maybe的实例,这对我来说很自然,但我不明白函数如何成为Functor甚至是Monad。

编辑: 好的,这是一个无法编译的有效良好类型的表达式:

fmap (+) (+) 1 (+1) 1

2 个答案:

答案 0 :(得分:4)

首先,我同意你的观点:函数作为仿函数并不是非常直观,事实上我有时希望这些实例不存在。并不是说它们有时没用,但是它们常常以不必要和混乱的方式使用。这些方法总是可以用更具体的组合子(特别是来自Control.Arrow)替换,或者用等效的方法替换,但在某种程度上更具描述性reader monad

那说......要理解功能仿函数,我建议你先考虑Map。在某种程度上,Map Int很像一个数组:它包含一些你可以转换的元素(即fmap),你可以通过索引来访问各个元素用整数。 Map只允许“数组”在其中有间隙,并从整数索引推广到任何可以排序的索引。

在另一个视图中,Map只是函数的特定实现:它将参数(键)与结果(值)相关联。这应该非常清楚函数仿函数是如何工作的:它会覆盖函数的所有可能结果

遗憾的是,这个解释并没有解释Monad实例,因为Map实际上并没有monad(甚至Applicative}实例。确实无法直接调整列表/数组实现...回顾:在列表中,我们有

pure x ≡ [x]
(,) <$> [a,b] <*> [x,y,z] ≡ [(a,x),(a,y),(a,z),(b,x),(b,y),(b,z)]

所以在合并之后,指数都是不同的。这对我们想支持通用密钥的Map无效。

但是,列表还有一个替代的monad实例, zip list

pure x ≡ repeat x
(,) <$> [a,b] <*> [x,y,z] ≡ [(a,x),(b,y)]

请注意,元素的索引会被保留。

现在,如果只有Map生成器,这个实例实际上可以适用于repeat :: a -> Map k a。这是不存在的,因为通常存在无限多的键,我们不能将它们全部枚举,也不能平衡这样的Map所带来的无限树。但是如果我们将自己限制在只有有限多个可能值的关键类型(例如Bool),那么我们就是好的:

instance Applicative (Map Bool) where
  pure x = Map.fromList [(False, x), (True, x)]
  <*> = Map.intersectWith ($)

现在,这正是monad函数的工作原理,与Map不同,如果可能有无数多个不同的参数就没有问题,因为你永远不会尝试将所有这些存储起来值;相反,你总是只在现场计算价值。

如果没有懒散的话,这是不可行的 - 在Haskell中几乎不是问题,事实上如果你在Map上进行fmap,它也会懒散地发生。对于函数函子,fmap实际上不仅仅是懒惰,但结果也会立即被遗忘,需要重新计算。

答案 1 :(得分:2)

函数

fmap对函数产生的结果起作用:

GHCi> :set -XTypeApplications
GHCi> :t fmap @((->) _)
fmap @((->) _) :: (a -> b) -> (t -> a) -> t -> b

a函数的t -> a结果通过a -> b函数进行修改。如果这听起来很像功能组合,那是因为它恰好是:

GHCi> fmap (3 *) (1 +) 4
15
GHCi> ((3 *) <$> (1 +)) 4
15
GHCi> ((3 *) . (1 +)) 4
15

(<*>)有点棘手:

GHCi> :t (<*>) @((->) _)
(<*>) @((->) _) :: (t -> a -> b) -> (t -> a) -> t -> b

Applicative f => f (a -> b)参数变为t -> (a -> b)(<*>)通过使用辅助函数(类型t -> a -> b)将两个参数(类型为t -> b)的函数转换为一个参数(类型为t -> a)的函数从第一个生成第二个参数:

GHCi> :t \k f -> \x -> k x (f x)
\k f -> \x -> k x (f x) :: (t2 -> t -> t1) -> (t2 -> t) -> t2 -> t1

以下是使用FunctorApplicative函数实例以应用样式编写的示例:

GHCi> ((&&) <$> (> 0) <*> (< 4)) 2
True

阅读它的一种方法是将2提供给(> 0)(< 4),然后将结果与(&&)&#34;结合起来。它可以使用来自liftA2的{​​{1}}以更紧凑的方式编写,但我相信Control.Applicative / (<$>)拼写更有意图揭示。

(<*>)Applicative ...

的另一种方法
pure

...从GHCi> :t pure @((->) _) pure @((->) _) :: a -> t -> a 中创建一个t -> a函数,而不是别的。常量函数是这样做的唯一方法:

a

请注意GHCi> pure 2 "foo" 2 GHCi> pure 2 42 2 在上面的每个示例中都有不同的类型。

鉴于上述所有情况,pure 2实例令人惊讶地无趣。为了更加清晰,请查看Monad而不是(=<<)

(>>=)

如果将此类型与GHCi> :t (=<<) @((->) _) (=<<) @((->) _) :: (a -> t -> b) -> (t -> a) -> t -> b 的类型进行比较,您将看到它们是相同的,只是第一个参数已被翻转。函数实例是一种特殊情况,其中(<*>)Applicative执行的操作基本相同。

值得一提的是,来自Monad的{​​{1}}可用于将值用作双参数函数的两个参数:

join