我刚刚意识到,函数有Monad,Functor和Applicative的实例。
当我看到一些我没有得到的类型类实例时,我通常会做的是编写一个类型很好的表达式并查看它返回的内容:
有人可以解释这些情况吗?你通常会听到List和Maybe的实例,这对我来说很自然,但我不明白函数如何成为Functor甚至是Monad。
编辑: 好的,这是一个无法编译的有效良好类型的表达式:
fmap (+) (+) 1 (+1) 1
答案 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
以下是使用Functor
和Applicative
函数实例以应用样式编写的示例:
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