函数的类型类实例
Typeclass instances for functions
我刚刚意识到,Functions 有 Monad、Functor 和 Applicative 的实例。
当我看到一些我没有得到的类型类实例时,我通常做的是写一些类型良好的表达式并看看它是什么 returns:
有人可以解释这些情况吗?您通常听说过 List 和 Maybe 的实例,这对我来说很自然,但我不明白 Functions 怎么可以是 Functor 甚至 Monad。
编辑:
好的,这是一个有效的类型良好的表达式,但无法编译:
fmap (+) (+) 1 (+1) 1
首先,我同意你的看法:函数不像函子那样直观,事实上我有时希望这些实例不存在。并不是说它们有时没有用,而是它们经常以不必要和令人困惑的方式使用。这些方法总是可以替换为更具体的组合器(特别是来自 Control.Arrow
),或者替换为等效但不知何故更具描述性的 reader monad.
也就是说...要理解函数函子,我建议你先考虑Map
。在某种程度上,Map Int
很像一个数组:它包含一些你可以转换的元素(即 fmap
over),你可以通过 indexing[=61 访问单个元素=] 他们与整数。 Map
只允许“数组”中有间隙,并从整数索引推广到任何可以排序的索引。
但从另一个角度来看,Map
只是 函数 的特定实现:它将参数(键)与结果(值)相关联。这应该很清楚 函数仿函数 是如何工作的:它 fmaps 函数的所有可能结果 †。
不幸的是,这个解释并没有很好地解释 Monad
实例,因为 Map
实际上没有 monad(甚至 Applicative
)实例。 list/array 实现的直接改编确实是不可能的……回顾一下:在列表中,我们有
pure x ≡ [x]
(,) <$> [a,b] <*> [x,y,z] ≡ [(a,x),(a,y),(a,z),(b,x),(b,y),(b,z)]
所以合并后,索引都是不同的。这不适用于 Map
我们想要支持通用键的地方。
但是,列表有一个替代的 monad 实例,zip 列表:
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 中几乎不是问题,事实上如果你 fmap在 Map
上它也懒惰地发生。对于function functor,fmap
其实不只是偷懒,结果也立马忘记了,需要重新计算
fmap
对于函数作用于函数产生的结果:
GHCi> :set -XTypeApplications
GHCi> :t fmap @((->) _)
fmap @((->) _) :: (a -> b) -> (t -> a) -> t -> b
t -> a
函数的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
类型)将两个参数(类型t -> a -> b
)的函数转换为一个参数(类型t -> b
)的函数从第一个参数生成第二个参数:
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
一种阅读方式是"feed 2
to (> 0)
and to (< 4)
, and combine the results with (&&)
"。它可以用 Control.Applicative
中的 liftA2
以更紧凑的方式编写,但我相信 (<$>)
/(<*>)
拼写更能揭示意图。
Applicative
的另一种方法,pure
...
GHCi> :t pure @((->) _)
pure @((->) _) :: a -> t -> a
... 从 a
中创建一个 t -> 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
做的事情本质上是相同的。
值得一提的是,Control.Monad
中的join
可用于将一个值用作双参数函数的两个参数:
GHCi> :t join @((->) _)
join @((->) _) :: (t -> t -> a) -> t -> a
GHCi> join (*) 5
25
我刚刚意识到,Functions 有 Monad、Functor 和 Applicative 的实例。
当我看到一些我没有得到的类型类实例时,我通常做的是写一些类型良好的表达式并看看它是什么 returns:
有人可以解释这些情况吗?您通常听说过 List 和 Maybe 的实例,这对我来说很自然,但我不明白 Functions 怎么可以是 Functor 甚至 Monad。
编辑: 好的,这是一个有效的类型良好的表达式,但无法编译:
fmap (+) (+) 1 (+1) 1
首先,我同意你的看法:函数不像函子那样直观,事实上我有时希望这些实例不存在。并不是说它们有时没有用,而是它们经常以不必要和令人困惑的方式使用。这些方法总是可以替换为更具体的组合器(特别是来自 Control.Arrow
),或者替换为等效但不知何故更具描述性的 reader monad.
也就是说...要理解函数函子,我建议你先考虑Map
。在某种程度上,Map Int
很像一个数组:它包含一些你可以转换的元素(即 fmap
over),你可以通过 indexing[=61 访问单个元素=] 他们与整数。 Map
只允许“数组”中有间隙,并从整数索引推广到任何可以排序的索引。
但从另一个角度来看,Map
只是 函数 的特定实现:它将参数(键)与结果(值)相关联。这应该很清楚 函数仿函数 是如何工作的:它 fmaps 函数的所有可能结果 †。
不幸的是,这个解释并没有很好地解释 Monad
实例,因为 Map
实际上没有 monad(甚至 Applicative
)实例。 list/array 实现的直接改编确实是不可能的……回顾一下:在列表中,我们有
pure x ≡ [x]
(,) <$> [a,b] <*> [x,y,z] ≡ [(a,x),(a,y),(a,z),(b,x),(b,y),(b,z)]
所以合并后,索引都是不同的。这不适用于 Map
我们想要支持通用键的地方。
但是,列表有一个替代的 monad 实例,zip 列表:
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 中几乎不是问题,事实上如果你 fmap在 Map
上它也懒惰地发生。对于function functor,fmap
其实不只是偷懒,结果也立马忘记了,需要重新计算
fmap
对于函数作用于函数产生的结果:
GHCi> :set -XTypeApplications
GHCi> :t fmap @((->) _)
fmap @((->) _) :: (a -> b) -> (t -> a) -> t -> b
t -> a
函数的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
类型)将两个参数(类型t -> a -> b
)的函数转换为一个参数(类型t -> b
)的函数从第一个参数生成第二个参数:
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
一种阅读方式是"feed 2
to (> 0)
and to (< 4)
, and combine the results with (&&)
"。它可以用 Control.Applicative
中的 liftA2
以更紧凑的方式编写,但我相信 (<$>)
/(<*>)
拼写更能揭示意图。
Applicative
的另一种方法,pure
...
GHCi> :t pure @((->) _)
pure @((->) _) :: a -> t -> a
... 从 a
中创建一个 t -> 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
做的事情本质上是相同的。
值得一提的是,Control.Monad
中的join
可用于将一个值用作双参数函数的两个参数:
GHCi> :t join @((->) _)
join @((->) _) :: (t -> t -> a) -> t -> a
GHCi> join (*) 5
25