a new born baby rest her head on the earth of mother
everything else is outer space.
新生的宝宝将头靠在妈妈的怀中
其余万物,尽属太空之外-- Mike Birbiglia, The New One
-- 1209
在终於解开那谜题之後,我听到了更大声的低鸣,我甚至感觉到震动并微微的晕眩了一下。是每次解开问题,建筑具现化後就会伴随着地震的现象吗?才闪过这个念头,那本紫书打开了後门飘走。跟着它离开建筑时,我发现那房间的侧面,有一扇标示着 over
, 旁边还悬挂着着棱镜的门。我还注意到,虽然刚刚有那麽大的地震,但悬着的棱镜,一点都没有晃动过的样子……但书实在是远到快看不见了,我只好赶快跟上……
书所停驻的这一栋建筑,上面标识着 <*>
,就坐落在我们刚离开的那栋与墙的中间。但相较之下,外观的感觉比之前的楼房,及後面的墙都要新上许多,似乎是相当晚近才盖起来的。而且虽然门上有许多痕迹,但气氛上却不知为何比较冷清一点。
当然那本飘在空中的书还是守在门旁,浮现着与之前类似格式的字:
Applicative 的实作:
class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b liftA2 :: (a -> b -> c) -> f a -> f b -> f c (*>) :: f a -> f b -> f b (<*) :: f a -> f b -> f a {-# MINIMAL pure, ((<*>) | liftA2) #-}
Applicative 的法则:
- 单位元素
- 结合律
- 同态
- 交换律
Applicative 的概念似乎是许多其它程序语言的使用者开始卡关的点,探究其原因,应该是惰性函式,以及函式组合的手法在其它语言里使用机率较少,因此用这两个概念继续往下延伸的手法想来也就更加乏人问津了。或者用另一个说法,在其它语言里,倾向用别的方式解决类似的问题。
fmap
的函式,需要两个参数怎麽办?要讨论 applicative,要先从 functor 开始。重新复习一下上一章的 fmap
,而这次我们的重点,放在第一个引数:传进去的函式上面:
-- Haskell 语法
fmap (\x -> x + 1) [1, 2] -- => [2, 3]
-- ^^^^^^^^^^^^^
-- 就是它
我们这次刻意把那个函式展开来并标记。这个位置一般都会用只接收一个参数的函式,因为之後就会用容器里一个个的元素当引数分别呼叫该函式。
那麽我们的问题是,如果我们在那个地方,传进一个需要两个参数的函式,会发生什麽事?
我们先在其它语言上示范一下:
# Elixir 语法
Enum.map([1, 2], fn x, y -> x + y end)
# 错误
# => ** (BadArityError) #Function<43.97283095/2 in :erl_eval.expr/5> with arity 2 called with 1 argument (1)
这个反应基本上想传达的意思是:
你不尊重函式。函式生气了。
原因你可能已经猜出来了,是因为这些语言里的函式,都是急躁的(比较好听的说法是积极执行的)。
注*:如果你试着在 JavaScript 里的 map
里传入需要两个、或三个参数的函式,会得到相当意外的结果。
然而 Haskell 的函式是惰性求值的,所以在正常呼叫函式时,当我们只喂给双参数函式一个引数时,会拿到一个部份应用的函式,重新复习一下:
-- Haskell 语法
f a b = a + b
g = f 1 -- 这是一个还没有饱和的函式,也就是个 *partial application*
那麽当我们对串列 fmap
,但是传进去的是需要二个以上参数的函式时,我们会得到的是装在串列里的部份应用函式。
-- Haskell 语法
f a b = a + b
fmap f [1, 2] -- => [ (1+), (2+) ]
重新再看一下上面写法的第一行,那个 f
,不就只是把中缀的 +
,变成前缀的格式而己吗?所以我们其实可以不要第一行的宣告,而把第四行改成这样写:
-- Haskell 语法
fmap (+) [1, 2] -- => [ (1+), (2+) ]
一样,我们拿到了装在串列里的函式。
那麽问题来了,这种装在容器里的函式要怎麽使用?
在此我们先作弊一下,先从 Maybe 这个比较简单的情况讲起。我们重新对一个 Maybe functor 用需要两个参数的函式 进行 fmap
:
-- Haskell 语法
fmap (+) $ Just 1 -- => Just (+1)
我们拿到一个装在 Maybe 容器里的部份应用函式。
接下来我们还要假设一个情况,就是我们想要应用 (apply) 到装在 Maybe 里那个函式的参数,也是被装在 Maybe 容器里的,例如说: (Just 2)
。
那麽我们需要一个可以接收这两种东西,并进行计算的函式:
-- Haskell
(Just (+1)) ??? (Just 2)
而上方 ???
位置所在的这个函式,就是 applicative 的 <*>
,有人把它叫做 apply,也有人叫它 ap。
我们再来比较一下这些二元运算的型别:
-- Haskell
($) :: (a -> b) -> a -> b -- 函式应用
(<$>) :: (a -> b) -> f a -> f b -- fmap
(<*>) :: f (a -> b) -> f a -> f b -- ap
注意到差别了吗?在最後一行的部份,连一开始的函式都是放在 f
容器里的。
让我们把 <*>
的型别定义用另一个方式排一下:
-- 把型别里,容器外壳跟里面的内容上下错开一点
(<*>) :: f (a -> b) -> f a -> f b
f f f --- 外壳
(a -> b) a b --- 内容
如果我们只看内容那一行的话,看起来就是个正常的函式应用。但注意到外壳的部份,我们可以看到我们把两个 f
,合并成一个 f
。而这个特性,我们在前不久才做过非常类似的事:monoid 的 mappend
。不过仔细看的话,在 Applicative 的型别定义里并没有 Monoid (只有少数几个有),因此有些时候,我们得要手动模拟出这个行为。
所以我们想要的是类似这样的东西:
mappend :: f f f
$ :: (a -> b) a b
(<*>) :: f (a -> b) -> f a -> f b
我们要来看一下 Maybe 上的 applicative 会怎麽运作了:
-- Haskell 语法
(+) <$> (Just 1) <*> (Just 2) -- => Just 3
(+) <$> (Just 1) <*> Nothing -- => Nothing
(+) <$> Nothing <*> (Just 2) -- => Nothing
只要後面任何一步出现 Nothing
,其结果就会是 Nothing
。而在容器内都有值的情况下,就会用里面的值去应用函式,接着再把结果装回合并再一起的函式里。
这个行为是怎麽实作出来的呢?
-- Haskell 语法
instance Applicative Maybe where
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
我们在前面装在容器里的函式是 Nothing 时,回传 Nothing。除此之外,将容器里的函式 f
用 functor 的特性,fmap
到後面的 Maybe 容器里去。
pure
而另外一个需要实作的函式 pure
,则是怎麽把任意元素装进这个容器里的方法,用另一种说法,这是一个接收值,回传把这个值包在容器里的函式。因此在 Maybe 的例子里,就是 Just
,所以完整的实作会长这样:
-- Haskell 语法
instance Applicative Maybe where
pure = Just -- 多了这行
Nothing <*> _ = Nothing
(Just f) <*> something = fmap f something
当我们用 pure
,把 id
这个函式装到容器里之後,再与任何容器进行 <*>
,後面的容器保持不变:
-- Haskell 语法
pure id <*> v -- => v
-- 示范
pure id <*> [1, 2, 3] -- => [1, 2, 3]
pure id <*> Just "test" -- => Just "test"
pure id <*> Left "wrong!" -- => Left "wrong!"
注意到用 pure
包装函式之後,这个容器的型别其实还没确定下来,等到要进行 <*>
的时候,就可以推测出要用哪一种容器来装函式了。
首先我们有两个装着函式的容器,例如 Just f
与 Just g
,还有一个装着值的容器比如说 Just x
。
那麽
先把两个容器里的函式结合,再去应用最後容器里的值
与
先用中间容器里的函式来应用後面的容器值,再把结果应用到前面容器里的函式
结果会是相同的。
但是为了要先结合容器里的函式,所以语法会… 有点奇怪:
pure (.) <*> f <*> g -- 把函式结合这件事也放到容器里,再去 ap 两个容器里的函式
-- 所以
pure (.) <*> f <*> g <*> x
=
f <*> (g <*> x)
-- 示范
pure (.) <*> Just (+10) <*> Just (*3) <*> Just 2 -- => Just 16
Just (+10) <*> (Just <*3> <*> Just 2) -- => Just 16
感觉有点类似封闭律,但重点在於容器里的函式应用,能保持外壳是相同的这件事。定义上是这麽写的:
pure f <*> pure x = pure (f x)
-- 示范
pure (*2) <*> pure 3
=
pure ((*2) 3)
也就是两个装在容器里的东西 ap
之後,其结果等同於先进行函式应用,再把结果装到容器里。
数学上的加法交换律是这样的:
跟据这条定律,那麽两个容器前後对调,再进行 <*>
结果不变。
但是… 那不是函式应用吗?可以把引数写在前面,函式写在後面吗?
可以的,我们有 $
。
--- Haskell
pure ($ 2) <*> Just (+10) -- => Just 12
你知道吗?只要能忽视那些不满的声音(摀耳朵),我觉得串列 []
根本就是这个城市的万用钥匙。 (丢
[to be continue]
<<: [Day 27]用Django架构建置专属的LINEBOT吧 - 用LINE进行影片画面边缘侦测处理
好快就过完三十天了,这系列文章也要结束了,还记得第一天的时候,希望可以涵盖一些主题,对应这三十天以来...
Combobox就是组合框,是tkinter.ttk的空件,所以要先import才可以用。他跟ope...
前言 「图」就是前面所有的大集合体,并从中衍生很多的内容,内容有点超乎想像的多,容小的慢慢写~~ 有...
by CSS Animations- Guide to Cubic Bezier Curves 关...
重点摘要: 可靠的研究中,没有白底黑底对眼健康的直接支持或反对 蓝光已知影响睡眠周期 对眼睛好要: ...