(这是 Typeclassopedia 中的练习。)
如何计算两个非平凡函数的组合类型,例如foldMap . foldMap
?
对于简单的情况,这很简单:只需查看(.)
(.) :: (b -> c) -> (a -> b) -> (a -> c)
找到两个函数的类型a
,b
和c
。
对于foldMap
,类型为
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
我认为无法将此功能的类型“拆分”为两部分,因此我可以在(.)
的类型中获得“a”,“b”和“c”。
然后我要求ghci
计算其类型。它成功地使用以下类型:
Prelude Data.Foldable> :t foldMap . foldMap
foldMap . foldMap
:: (Foldable t, Foldable t1, Data.Monoid.Monoid m) =>
(a -> m) -> t (t1 a) -> m
如何从foldMap
和(.)
类型派生该类型?对于t (t1 a)
类型中未找到的“新”类型foldMap
如何显示在foldMap . foldMap
的类型中,我感到特别困惑。
答案 0 :(得分:15)
在简单的情况下工作的相同的等式推理技术将继续在这个更复杂的情况下工作。需要记住的一个重要事项是,->
是正确关联的;这意味着a -> b -> c
与a -> (b -> c)
相同;因此,Haskell中的所有函数只接受一个输入并产生一个输出,因此可以组合。 (这种等价性是在任何地方进行部分应用的能力背后的原因。)因此,我们可以将foldMap
的类型签名重写为
foldMap :: (Foldable t, Monoid m) => (a -> m) -> (t a -> m)
为清楚起见,我将给出foldMap
两个不同名称的不同名称,并为其类型变量使用不同的名称;我们将foldMap₂ . foldMap₁
,其中
foldMap₁ :: (Foldable s, Monoid n) => (a -> n) -> (s a -> n)
foldMap₂ :: (Foldable t, Monoid m) => (b -> m) -> (t b -> m)
(.) :: (d -> e) -> (c -> d) -> (c -> e)
因此,必须是
的情况foldMap₂ . foldMap₁ :: c -> e
但c
和e
是什么,d
是什么让它起作用?离开阶级限制(他们只是在最后联合在一起,并且会在整个过程中混乱一切),我们知道
foldMap₂ . foldMap₁ ---+
|
|
/-------foldMap₂-------\ /-------foldMap₁-------\ /---+--\
(.) :: (d -> e ) -> (c -> d ) -> (c -> e)
((b -> m) -> (t b -> m)) -> ((a -> n) -> (s a -> n))
这会产生以下等值(请记住Haskell拼写类型相等~
):
(c -> d) ~ ((a -> n) -> (s a -> n))
(d -> e) ~ ((b -> m) -> (t b -> m))
因为这些是函数类型的等价,我们知道域和范围分别相等:
c ~ (a -> n)
e ~ (t b -> m)
d ~ (b -> m)
d ~ (s a -> n)
我们可以通过传递性来贬低d
等于
(b -> m) ~ (s a -> n)
然后,由于双方都是函数类型,我们可以打破这个平等,得出结论
b ~ s a
m ~ n
所以d ~ (s a -> n)
,或者换句话说只是foldMap₁
的结果类型 - 诀窍是b -> m
,foldMap₂
的输入类型,通用到足以统一与前者类型! (这里,统一是类型推理器所做的;当更多特定类型替换为类型变量时,两种类型可以统一。)
最后,替换为c
和e
,我们得到
(c -> e) ~ ((a -> n) -> e) by the equality for c
~ ((a -> n) -> (t b -> m)) by the equality for e
~ ((a -> m) -> (t b -> m)) by the equality for n
~ ((a -> m) -> (t (s a) -> m)) by the equality for b
因此,当我们添加完整的类约束列表时(请记住Monoid m
和Monoid n
实际上是相同的约束,因为m ~ n
)并丢弃冗余的括号对,我们得到
foldMap . foldMap :: (Foldable s, Foldable t, Monoid m)
=> (a -> m) -> t (s a) -> m
其中,重命名,与GHCi给你的相同。
请注意最后一步,即嵌套类型t (s a)
出现的位置。这来自上面b
的统一,在d
的平等内部。我们知道某些foldMap₂ . foldMap₁
的{{1}}的结果类型为t b -> m
;碰巧将b
的输出和foldMap₁
约束foldMap₂
的输入统一为类型b
。我们总是可以用更复杂的类型表达式来统一类型变量(只要更复杂的表达式不涉及原始类型变量; s a
和b
将无法统一),这有时会导致有趣的类似t b
的类型,当它发生在幕后。
答案 1 :(得分:2)
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
也可以被视为
foldMap :: (Foldable t, Monoid m) => (a -> m) -> (t a -> m)
因为->
与右边相关联。插入
(.) :: (y -> z) -> (x -> y) -> (x -> z)
我们得到了
x = (Monoid m) => a -> m
y = (Foldable ty, Monoid m) => ty a -> m
在这里,您必须将a
替换为ty a
中的foldMap
:
z = (Foldable ty, Foldable tz, Monoid m) => tz (ty a) -> m
你从(.)
获得的是
x -> z
这只是另一种说法
(Foldable ty, Foldable tz, Monoid m) => (a -> m) -> (tz (ty a) -> m)
,当删除不必要的括号时
(Foldable ty, Foldable tz, Monoid m) => (a -> m) -> tz (ty a) -> m
或 - 如ghci
写的那样 -
(Foldable t, Foldable t1, Monoid m) => (a -> m) -> t (t1 a) -> m
答案 2 :(得分:2)
暂时省略类型类约束,可以写出foldMap
和(.)
的签名:
foldMap :: (a -> m) -> (t a -> m)
(.) :: (y -> z) -> (x -> y) -> (x -> z) -- this is just a change of variables
这里我们使用了函数应用程序关联到右边的事实。
因此,分析foldMap . foldMap
的类型签名会设置这些对应关系:
foldMap . foldMap
(a -> m) -> (t a -> m) ( a' -> m') -> (t' a' -> m') (a' -> m') -> (t a -> m)
(y -> z) -> (x -> y) -> (x -> z)
即。我们有以下类型的平等:
y = a -> m
z = t a -> m
x = a' -> m'
y = t' a' -> m'
x = a' -> m'
z = t a -> m
减少到:
a = t' a'
m = m'
即。 foldMap . foldMap
的类型为(a' -> m) -> (t (t' a') -> m)
或等效(a' -> m) -> t (t' a) -> m
。