关于Typeclassopedia中Applicative组合律的理解正确性验证问询
嘿,我太懂这种「GHCI验证过定律成立,但脑子还是转不过弯」的感觉了!咱们把Applicative的组合律拆成小块,一步步捋清楚,你肯定能get到它的本质~
先明确组合律的标准形式
首先先把组合律的表达式摆出来,方便咱们对照:
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
你说搞不懂右侧的嵌套<*>?其实核心是要先搞懂<*>的作用,以及每一步的类型变化——咱们从类型和具体例子两个维度拆解。
从类型视角拆解左右两边
先回忆<*>的类型签名:
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
它的作用是把Applicative上下文里的函数,应用到上下文里的值上。而pure (.)则是把普通的函数组合运算符(.)(类型(b->c)->(a->b)->a->c)放到了Applicative上下文里,变成f ((b->c)->(a->b)->a->c)。
左侧表达式:((pure (.) <*> u) <*> v) <*> w
因为<*>是左结合的,咱们一步步展开:
- 第一步:
pure (.) <*> uu的类型是f (b -> c),这一步相当于把上下文里的(.)和u里的函数组合,得到f ((a -> b) -> a -> c)——简单说就是「一个能接受a->b函数、返回a->c函数的上下文」。
- 第二步:
(pure (.) <*> u) <*> vv的类型是f (a -> b),这一步把上面的结果和v里的函数组合,得到f (a -> c)——也就是「一个能把a转换成c的函数的上下文」。
- 第三步:最终
<*> ww的类型是f a,把上下文里的函数应用到上下文里的a上,最终得到f c。
右侧表达式:u <*> (v <*> w)
这次咱们先算括号里的部分:
- 第一步:
v <*> wv是f (a -> b),w是f a,这一步直接把v里的函数应用到w里的值上,得到f b。
- 第二步:
u <*> (v <*> w)u是f (b -> c),把u里的函数应用到f b上,最终也得到f c。
你看,左右两边最终的类型完全一致,而且逻辑上都是把u、v里的函数依次应用到w的值上——只是顺序不同:左侧是先组合函数再应用,右侧是先应用内层函数再应用外层函数。
用具体例子直观感受
咱们用最常见的Maybe函子举个例子,一看就明白:
- 设
u = Just (+2)(类型Maybe (Int -> Int)) - 设
v = Just (*3)(类型Maybe (Int -> Int)) - 设
w = Just 4(类型Maybe Int)
计算左侧
pure (.) <*> Just (+2) <*> Just (*3) <*> Just 4 -- 第一步:pure (.) <*> Just (+2) → Just ((+2) .) -- 第二步:Just ((+2) .) <*> Just (*3) → Just ((+2) . (*3)) -- 第三步:Just ((+2) . (*3)) <*> Just 4 → Just ((+2) ((*3) 4)) → Just 14
计算右侧
Just (+2) <*> (Just (*3) <*> Just 4) -- 第一步:Just (*3) <*> Just 4 → Just 12 -- 第二步:Just (+2) <*> Just 12 → Just 14
结果完全一致!这就是组合律要保证的:不管是先组合函数再应用,还是先应用内层函数再套外层,最终的结果是一样的。
关于你发现的「组合对两个函子有效」
其实这正是Applicative组合律的威力所在——它不仅对单一Applicative(比如Maybe)成立,对嵌套的Applicative(比如Maybe (IO Int)这种两层函子,只要每层都满足Applicative定律)也成立。本质上是在保证:函数组合的语义,在任何Applicative上下文里都能和普通函数组合保持一致,不会因为上下文的存在而「变形」。
内容的提问来源于stack exchange,提问作者melston

