Int
的行为跟很多东西很像。好比说他可以比较相不相等,可以从大到小排列,也可以将他们一一穷举出来。Maybe
,trees 等等。在 Haskell 中我们是用 Functor
这个 typeclass 来描述他。这个 typeclass 只有一个 method,叫做 fmap
,他的型态是 fmap :: (a -> b) -> f a -> f b
。这型态说明了如果给我一个从 a
映到 b
的函数,以及一个装了 a
的盒子,我会回给你一个装了 b
的盒子。就好像用这个函数将每个元素都转成 b
一样Functor
的 instance,那他的 kind 必须是 * -> *
,这代表他必须刚好接受一个 type 当作 type parameter。像是 Maybe
可以是 Functor 的一个 instance,因为他接受一个 type parameter,来做成像是 Maybe Int
,或是 Maybe String
。如果一个 type constructor 接受两个参数,像是 Either
,我们必须给他两个 type parameter。所以我们不能这样写:instance Functor Either where
,但我们可以写 instance Functor (Either a) where
,如果我们把 fmap
限缩成只是 Either a
的,那他的型态就是 fmap :: (b -> c) -> Either a b -> Either a c
。就像你看到的,Either a
的是固定的一部分,因为 Either a
只恰好接受一个 type parameter,但 Either
则要接受两个 type parameters。这样 fmap 的型态变成 fmap :: (b -> c) -> Either b -> Either c
,这不太合理。Functor
的 instance,像是 []
,Maybe
,Either a
以及我们自己写的 Tree
。我们也看到了如何用一个函数 map 他们。在这一章节,我们再多举两个例子,也就是 IO
跟 (->) r
。IO String
,他代表的是一个会被计算成 String 结果的 I/O action。我们可以用 do syntax 来把结果绑定到某个名称。我们之前把 I/O action 比喻做长了脚的盒子,会到真实世界帮我们取一些值回来。我们可以查看他们取了什么值,但一旦看过,我们必须要把值放回盒子中。用这个比喻,IO
的行为就像是一个 functor。IO
是怎么样的一个 Functor
instance。当我们 fmap
用一个 function 来 map over I/O action 时,我们会想要拿回一个装着已经用 function 映射过值的 I/O action。fmap
的实作中,我们先执行了原本传进的 I/O action,并把结果绑定成 result
。然后我们写了 return (f result)
。return
就如你所知道的,是一个只会回传包了你传给他东西的 I/O action。还有一个 do block 的回传值一定是他最后一个 I/O action 的回传值。这也是为什么我们需要 return。其实他只是回传包了 f result
的 I/O action。fmap
来改写:fmap
reverse
来 map over Just "blah"
会得到 Just "halb"
,我们也可以 fmap
reverse
来 map over getLine
。getLine
是一个 I/O action,他的 type 是 IO String
,而用 reverse
来 map over 他会回传一个取回一个字串并 reverse
他的 I/O action。就像我们 apply 一个 function 到一个 Maybe
一样,我们也可以 apply 一个 function 到一个 IO
,只是这个 IO
会跑去外面拿回某些值。然后我们把结果用 <-
绑定到某个名称,而这个名称绑定的值是已经 reverse
过了。fmap (++"!") getLine
这个 I/O action 表现得就像 getLine
,只是他的结果多了一个 "!"
在最后。fmap
到 IO
型态上,那 fmap 的型态是 fmap :: (a -> b) -> IO a -> IO b
。fmap
接受一个函数跟一个 I/O action,并回传一个 I/O action 包含了已经 apply 过 function 的结果。fmap
,那会更漂亮地表达这件事。或者你想要对 functor 中的数据做 transformation,你可以先将你要用的 function 写在 top level,或是把他作成一个 lambda function,甚至用 function composition。intersperse '-' . reverse . map toUpper
合成了一个 function,他接受一个字串,将他转成大写,然后反过来,再用 intersperse '-'
安插'-'。他是比较漂亮版本的 (\xs -> intersperse '-' (reverse (map toUpper xs)))
。Functor
的案例是 (->) r
,只是我们先前没有注意到。你可能会困惑到底 (->) r
究竟代表什么?一个 r -> a
的型态可以写成 (->) r a
,就像是 2 + 3
可以写成 (+) 2 3
一样。我们可以从一个不同的角度来看待 (->) r a
,他其实只是一个接受两个参数的 type constructor,好比 Either
。但记住我们说过 Functor
只能接受一个 type constructor。这也是为什么 (->)
不是 Functor
的一个 instance,但 (->) r
则是。如果程序的语法允许的话,你也可以将 (->) r
写成 (r ->)
。就如 (2+)
代表的其实是 (+) 2
。至于细节是如何呢?我们可以看看 Control.Monad.Instances
。fmap
的型态。他的型态是 fmap :: (a -> b) -> f a -> f b
。我们把所有的 f
在心里代换成 (->) r
。则 fmap
的型态就变成 fmap :: (a -> b) -> ((->) r a) -> ((->) r b)
。接着我们把 (->) r a
跟 (->) r b
换成 r -> a
跟 r -> b
。则我们得到 fmap :: (a -> b) -> (r -> a) -> (r -> b)
。Maybe
会得到一个 Maybe
,而 map over 一个 function 到一个 list 会得到一个 list。而 fmap :: (a -> b) -> (r -> a) -> (r -> b)
告诉我们什么?他接受一个从 a
到 b
的 function,跟一个从 r
到 a
的 function,并回传一个从 r
到 b
的 function。这根本就是 function composition。把 r -> a
的输出接到 a -> b
的输入,的确是 function composition 在做的事。如果你再仔细看看 instance 的定义,会发现真的就是一个 function composition。fmap
当 composition 在用。可以用 :m + Control.Monad.Instances
把模块装载进来,并做一些尝试。fmap
的方式是 infix 的方式,这跟 .
很像。在第二行,我们把 (*3)
map over 到 (+100)
上,这会回传一个先把输入值 (+100)
再 (*3)
的 function,我们再用 1
去调用他。fmap (+3)
map over Just 3
的时候,对于 Maybe
我们很容易把他想成是装了值的盒子,我们只是对盒子里面的值 (+3)
。但对于 fmap (*3) (+100)
呢?你可以把 (+100)
想成是一个装了值的盒子。有点像把 I/O action 想成长了脚的盒子一样。对 (+100)
使用 fmap (*3)
会产生另一个表现得像 (+100)
的 function。只是在算出值之前,会再多计算 (*3)
。这样我们可以看出来 fmap
表现得就像 .
一样。fmap
等同于 function composition 这件事对我们来说并不是很实用,但至少是一个有趣的观点。这也让我们打开视野,看到盒子的比喻不是那么恰当,functors 其实比较像 computation。function 被 map over 到一个 computation 会产生经由那个 function 映射过后的 computation。fmap
该遵守的规则之前,我们再看一次 fmap
的型态,他是 fmap :: (a -> b) -> f a -> f b
。很明显我们是在讨论 Functor,所以为了简洁,我们就不写 (Functor f) =>
的部份。当我们在学 curry 的时候,我们说过 Haskell 的 function 实际上只接受一个参数。一个型态是 a -> b -> c
的函数实际上是接受 a
然后回传 b -> c
,而 b -> c
实际上接受一个 b
然后回传一个 c
。如果我们用比较少的参数调用一个函数,他就会回传一个函数需要接受剩下的参数。所以 a -> b -> c
可以写成 a -> (b -> c)
。这样 curry 可以明显一些。fmap
想成是一个接受 function 跟 functor 并回传一个 function 的 function。而是想成一个接受 function 并回传一个新的 function 的 function,回传的 function 接受一个 functor 并回传一个 functor。他接受 a -> b
并回传 f a -> f b
。这动作叫做 lifting。我们用 GHCI 的 :t
来做的实验。fmap (*2)
接受一个 functor f
,并回传一个基于数字的 functor。那个 functor 可以是 list,可以是 Maybe
,可以是 Either String
。fmap (replicate 3)
可以接受一个基于任何型态的 functor,并回传一个基于 list 的 functor。fmap (++"!")
,的时候会显得更清楚,fmap
想做是一个函数,他接受另一个函数跟一个 functor,然后把函数对 functor 每一个元素做映射,或你可以想做他是一个函数,他接受一个函数并把他 lift 到可以在 functors 上面操作。两种想法都是正确的,而且在 Haskell 中是等价。fmap (replicate 3) :: (Functor f) => f a -> f [a]
这样的型态代表这个函数可以运作在任何 functor 上。至于确切的行为则要看究竟我们操作的是什么样的 functor。如果我们是用 fmap (replicate 3)
对一个 list 操作,那我们会选择 fmap
针对 list 的实作,也就是只是一个 map
。如果我们是碰到 Maybe a
。那他在碰到 Just
型态的时候,会对里面的值套用 replicate 3
。而碰到 Nothing
的时候就回传 Nothing
。fmap
应该是要用一个函数 map 每一个元素,不多做任何事情。这些行为都被 functor laws 所描述。对于 Functor
的 instance 来说,总共两条定律应该被遵守。不过他们不会在 Haskell 中自动被检查,所以你必须自己确认这些条件。id
,那得到的新的 functor 应该要跟原来的一样。如果写得正式一点,他代表 fmap id = id
。基本上他就是说对 functor 调用 fmap id
,应该等同于对 functor 调用 id
一样。毕竟 id
只是 identity function,他只会把参数照原样丢出。他也可以被写成 \x -> x
。如果我们对 functor 的概念就是可以被 map over 的对象,那 fmap id = id
的性就显而易见。Maybe
的 fmap
的实作,我们不难发现第一定律为何被遵守。f
的位置摆上 id
。我们看到 fmap id
拿到 Just x
的时候,结果只不过是 Just (id x)
,而 id
有只回传他拿到的东西,所以可以知道 Just (id x)
等价于 Just x
。所以说我们可以知道对 Maybe
中的 Just
用 id
去做 map over 的动作,会拿回一样的值。id
map over Nothing
会拿回 Nothing
并不稀奇。所以从这两个 fmap
的实作,我们可以看到的确 fmap id = id
有被遵守。fmap (f . g) = fmap f . fmap g
。或是用另外一种写法,对于任何一个 functor F,下面这个式子应该要被遵守:fmap (f . g) F = fmap f (fmap g F)
。fmap
,我们知道不会有除了 mapping 以外的事会发生,而他就仅仅会表现成某个可以被 map over 的东西。也就是一个 functor。你可以再仔细查看 fmap
对于某些型别的实作来了解第二定律。正如我们先前对 Maybe
查看第一定律一般。Maybe
是如何遵守第二定律的。首先 fmap (f . g)
来 map over Nothing
的话,我们会得到 Nothing
。因为用任何函数来 fmap
Nothing
的话都会回传 Nothing
。如果我们 fmap f (fmap g Nothing)
,我们会得到 Nothing
。可以看到当面对 Nothing
的时候,Maybe
很显然是遵守第二定律的。 那对于 Just something
呢?如果我们使用 fmap (f . g) (Just x)
的话,从实作的代码中我可以看到 Just ((f . g ) x)
,也就是 Just (f (g x))
。如果我们使用 fmap f (fmap g (Just x))
的话我们可以从实作知道 fmap g (Just x)
会是 Just (g x)
。fmap f (fmap g (Just x))
跟 fmap f (Just (g x))
相等。而从实作上这又会相等于 Just (f (g x))
。Functor
的 instance,但实际上他们并不是 functor,因为他们并不遵守这些定律。我们来看看其中一个型别。Maybe a
的型别,只差在 Just
包含了两个 field 而不是一个。在 CJust
中的第一个 field 是 Int
,他是扮演计数器用的。而第二个 field 则为型别 a
,他是从型别参数来的,而他确切的型别当然会依据我们选定的 CMaybe a
而定。我们来对他作些操作来获得些操作上的直觉吧。CNothing
,就代表不含有 field。如果我们用的是 CJust
,那第一个 field 是整数,而第二个 field 可以为任何型别。我们来定义一个 Functor
的 instance,这样每次我们使用 fmap
的时候,函数会被套用在第二个 field,而第一个 field 会被加一。Maybe
的定义方式,只差在当我们使用 fmap
的时候,如果碰到的不是空值,那我们不只会套用函数,还会把计数器加一。我们可以来看一些范例操作。id
来 map over 一个 functor 的时候,他的结果应该跟只对 functor 调用 id
的结果一样。但我们可以看到这个例子中,这对于 CMaybe
并不遵守。尽管他的确是 Functor
typeclass 的一个 instace。但他并不遵守 functor law 因此不是一个 functor。如果有人使用我们的 CMaybe
型别,把他当作 functor 用,那他就会期待 functor laws 会被遵守。但 CMaybe
并没办法满足,便会造成错误的程序。当我们使用一个 functor 的时候,函数合成跟 map over 的先后顺序不应该有影响。但对于 CMaybe
他是有影响的,因为他纪录了被 map over 的次数。如果我们希望 CMaybe
遵守 functor law,我们必须要让 Int
字段在做 fmap
的时候维持不变。fmap
不会做多余的事情,只是用一个函数做映射而已。这让写出来的代码足够抽象也容易扩展。因为我们可以用定律来推论型别的行为。Functor
的 instance 都遵守这些定律,但你可以自己检查一遍。下一次你定义一个型别为 Functor
的 instance 的时候,花点时间确认他确实遵守 functor laws。一旦你操作过足够多的 functors 时,你就会获得直觉,知道他们会有什么样的性质跟行为。而且 functor laws 也会觉得显而易见。但就算没有这些直觉,你仍然可以一行一行地来找看看有没有反例让这些定律失效。Just 3
就是输出 3
,但他又带有一个可能没有值的 context。[1,2,3]
输出三个值,1
,2
跟 3
,同时也带有可能有多个值或没有值的 context。(+3)
则会带有一个依赖于参数的 context。fmap (+3) [1,2,3]
的时候,我们是把 (+3)
接到 [1,2,3]
后面,所以当我们查看任何一个 list 的输出的时候,(+3)
也会被套用在上面。另一个例子是对函数做 map over。当我们做 fmap (+3) (*3)
,我们是把 (+3)
这个转换套用在 (*3)
后面。这样想的话会很自然就会把 fmap
跟函数合成关联起来(fmap (+3) (*3)
等价于 (+3) . (*3)
,也等价于 \x -> ((x*3)+3)
),毕竟我们是接受一个函数 (*3)
然后套用 (+3)
转换。最后的结果仍然是一个函数,只是当我们喂给他一个数字的时候,他会先乘上三然后做转换加上三。这基本上就是函数合成在做的事。Control.Applicative
中的 Applicative
这个 typeclass 来定义的。a -> b -> c
,我们通常会说这个函数接受两个参数并回传 c
,但他实际上是接受 a
并回传一个 b -> c
的函数。这也是为什么我们可以用 (f x) y
的方式调用 f x y
。这个机制让我们可以 partially apply 一个函数,可以用比较少的参数调用他们。可以做成一个函数再喂给其他函数。Just 3
然后我们做 fmap (*) (Just 3)
,那我们会获得什么样的结果?从 Maybe
对 Functor
的 instance 实作来看,我们知道如果他是 Just something
,他会对在 Just
中的 something
做映射。因此当 fmap (*) (Just 3)
会得到 Just ((*) 3)
,也可以写做 Just (* 3)
。我们得到了一个包在 Just
中的函数。compare
到一个包含许多字符的 list 呢?他的型别是 (Ord a) => a -> a -> Ordering
,我们会得到包含许多 Char -> Ordering
型别函数的 list,因为 compare
被 partially apply 到 list 中的字符。他不是包含许多 (Ord a) => a -> Ordering
的函数,因为第一个 a
碰到的型别是 Char
,所以第二个 a
也必须是 Char
。Just (3 *)
还有另一个 functor 里面是 Just 5
,但我们想要把第一个 Just (3 *)
map over Just 5
呢?如果是普通的 functor,那就没救了。因为他们只允许 map 一个普通的函数。即使我们用 \f -> f 9
来 map 一个装了很多函数的 functor,我们也是使用了普通的函数。我们是无法单纯用 fmap
来把包在一个 functor 的函数 map 另一个包在 functor 中的值。我们能用模式匹配 Just
来把函数从里面抽出来,然后再 map Just 5
,但我们是希望有一个一般化的作法,对任何 functor 都有效。Applicative
这个 typeclass。他位在 Control.Applicative
中,在其中定义了两个函数 pure
跟 <*>
。他并没有提供缺省的实作,如果我们想使用他必须要为他们 applicative functor 的实作。typeclass 定义如下:Applicative
的定义,并加上 class contraint。描述了一个型别构造子要是 Applicative
,他必须也是 Functor
。这就是为什么我们说一个型别构造子属于 Applicative
的话,他也会是 Functor
,因此我们能对他使用 fmap
。pure
。他的型别宣告是 pure :: a -> f a
。f
代表 applicative functor 的 instance。由于 Haskell 有一个优秀的型别系统,其中函数又是将一些参数映射成结果,我们可以从型别宣告中读出许多消息。pure
应该要接受一个值,然后回传一个包含那个值的 applicative functor。我们这边是用盒子来作比喻,即使有一些比喻不完全符合现实的情况。尽管这样,a -> f a
仍有许多丰富的信息,他确实告诉我们他会接受一个值并回传一个 applicative functor,里面装有结果。pure
比较好的说法是把一个普通值放到一个缺省的 context 下,一个最小的 context 但仍然包含这个值。<*>
也非常有趣。他的型别是 f (a -> b) -> f a -> f b
。这有让你联想到什么吗?没错!就是 fmap :: (a -> b) -> f a -> f b
。他有点像加强版的 fmap
。然而 fmap
接受一个函数跟一个 functor,然后套用 functor 之中的函数。<*>
则是接受一个装有函数的 functor 跟另一个 functor,然后取出第一个 functor 中的函数将他对第二个 functor 中的值做 map。Maybe
的 Applicative
实作:f
作为 applicative functor 会接受一个具体型别当作参数,所以我们是写成 instance Applicative Maybe where
而不是写成 instance Applicative (Maybe a) where
。pure
。他只不过是接受一个东西然后包成 applicative functor。我们写成 pure = Just
是因为 Just
不过就是一个普通函数。我们其实也可以写成 pure x = Just x
。<*>
。我们无法从 Nothing
中抽出一个函数,因为 Nothing
并不包含一个函数。所以我们说如果我们要尝试从 Nothing
中取出一个函数,结果必定是 Nothing
。如果你看看 Applicative
的定义,你会看到他有 Functor
的限制,他代表 <*>
的两个参数都会是 functors。如果第一个参数不是 Nothing
,而是一个装了函数的 Just
,而且我们希望将这个函数对第二个参数做 map。这个也考虑到第二个参数是 Nothing
的情况,因为 fmap
任何一个函数至 Nothing
会回传 Nothing
。Maybe
而言,如果左边是 Just
,那 <*>
会从其中抽出了一个函数来 map 右边的值。如果有任何一个参数是 Nothing
。那结果便是 Nothing
。pure (+3)
跟 Just (+3)
在这个 case 下是一样的。如果你是在 applicative context 底下跟 Maybe
打交道的话请用 pure
,要不然就用 Just
。前四个输入展示了函数是如何被取出并做 map 的动作,但在这个 case 底下,他们同样也可以用 unwrap 函数来 map over functors。最后一行比较有趣,因为我们试着从 Nothing
取出函数并将他 map 到某个值。结果当然是 Nothing
。<*>
是 left-associative,也就是说 pure (+) <*> Just 3 <*> Just 5
可以写成 (pure (+) <*> Just 3) <*> Just 5
。首先 +
是摆在一个 functor 中,在这边刚好他是一个 Maybe
。所以首先,我们有 pure (+)
,他等价于 Just (+)
。接下来由于 partial application 的关系,Just (+) <*> Just 3
等价于 Just (3+)
。把一个 3
喂给 +
形成另一个只接受一个参数的函数,他的效果等于加上 3。最后 Just (3+) <*> Just 5
被运算,其结果是 Just 8
。pure f <*> x <*> y <*> ...
就让我们可以拿一个接受多个参数的函数,而且这些参数不一定是被包在 functor 中。就这样来套用在多个在 functor context 的值。这个函数可以吃任意多的参数,毕竟 <*>
只是做 partial application 而已。pure f <*> x
等于 fmap f x
的话,这样的用法就更方便了。这是 applicative laws 的其中一条。我们稍后会更仔细地查看这条定律。现在我们先依直觉来使用他。就像我们先前所说的,pure
把一个值放进一个缺省的 context 中。如果我们要把一个函数放在一个缺省的 context,然后把他取出并套用在放在另一个 applicative functor 的值。我们会做的事就是把函数 map over 那个 applicative functor。但我们不会写成 pure f <*> x <*> y <*> ...
,而是写成 fmap f x <*> y <*> ...
。这也是为什么 Control.Applicative
会 export 一个函数 <gt;
,他基本上就是中缀版的 fmap
。他是这么被定义的:<gt;
的使用显示了 applicative style 的好处。如果我们想要将 f
套用三个 applicative functor。我们可以写成 f <gt; x <*> y <*> z
。如果参数不是 applicative functor 而是普通值的话。我们则写成 f x y z
。Just "johntra"
跟 Just "volta"
这样的值,我们希望将他们结合成一个 String
,并且包含在 Maybe
中。我们会这样做:<gt;
跟 <*>
就可以把函数变成 applicative style,可以操作 applicatives 并回传 applicatives。(++) <gt; Just "johntra" <*> Just "volta"
时,首先我们将 (++)
map over 到 Just "johntra"
,然后产生 Just ("johntra"++)
,其中 (++)
的型别为 (++) :: [a] -> [a] -> [a]
,Just ("johntra"++)
的型别为 Maybe ([Char] -> [Char])
。注意到 (++)
是如何吃掉第一个参数,以及我们是怎么决定 a
是 Char
的。当我们做 Just ("johntra"++) <*> Just "volta"
,他接受一个包在 Just
中的函数,然后 map over Just "volta"
,产生了 Just "johntravolta"
。如果两个值中有任意一个为 Nothing
,那整个结果就会是 Nothing
。Maybe
当作我们的案例,你可能也会想说 applicative functor 差不多就等于 Maybe
。不过其实有许多其他 Applicative
的 instance。我们来看看有哪些。[]
为 Applicative
的 instance 的。pure
是把一个值放进缺省的 context 中。换种说法就是一个会产生那个值的最小 context。而对 list 而言最小 context 就是 []
,但由于空的 list 并不包含一个值,所以我们没办法把他当作 pure
。这也是为什么 pure
其实是接受一个值然后回传一个包含单元素的 list。同样的,Maybe
的最小 context 是 Nothing
,但他其实表示的是没有值。所以 pure
其实是被实作成 Just
的。<*>
呢?如果我们假定 <*>
的型别是限制在 list 上的话,我们会得到 (<*>) :: [a -> b] -> [a] -> [b]
。他是用 list comprehension 来实作的。<*>
必须要从左边的参数取出函数,将他 map over 右边的参数。但左边的 list 有可能不包含任何函数,也可能包含一个函数,甚至是多个函数。而右边的 list 有可能包含多个值。这也是为什么我们用 list comprehension 的方式来从两个 list 取值。我们要对左右任意的组合都做套用的动作。而得到的结果就会是左右两者任意组合的结果。<*>
是 left-associative,也就是说 [(+),(*)] <*> [1,2]
会先运作,产生 [(1+),(2+),(1*),(2*)]
。由于左边的每一个函数都套用至右边的每一个值。也就产生 [(1+),(2+),(1*),(2*)] <*> [3,4]
,其便是最终结果。100
或是 "what"
这样的值则是 deterministic 的计算,只会有一个结果。而 [1,2,3]
则可以看作是没有确定究竟是哪一种结果。所以他代表的是所有可能的结果。当你在做 (+) <gt; [1,2,3] <*> [4,5,6]
,你可以想做是把两个 non-deterministic 的计算做 +
,只是他会产生另一个 non-deterministic 的计算,而且结果更加不确定。[2,5,10]
跟 [8,10,11]
相乘的结果,所以我们这样做:*
。如果我们想要所有相乘大于 50 可能的计算结果,我们会这样写:pure f <*> xs
等价于 fmap f xs
。而 pure f
就是 [f]
,而且 [f] <*> xs
可将左边的每个函数套用至右边的每个值。但左边其实只有一个函数,所以他做起来就像是 mapping。Applicative
的 instance 是 IO
,来看看他是怎么实作的:pure
是把一个值放进最小的 context 中,所以将 return
定义成 pure
是很合理的。因为 return
也是做同样的事情。他做了一个不做任何事情的 I/O action,他可以产生某些值来作为结果,但他实际上并没有做任何 I/O 的动作,例如说印出结果到终端或是文件。<*>
被限定在 IO
上操作的话,他的型别会是 (<*>) :: IO (a -> b) -> IO a -> IO b
。他接受一个产生函数的 I/O action,还有另一个 I/O action,并从以上两者创造一个新的 I/O action,也就是把第二个参数喂给第一个参数。而得到回传的结果,然后放到新的 I/O action 中。我们用 do 的语法来实作他。你还记得的话 do 就是把好几个 I/O action 黏在一起,变成一个大的 I/O action。Maybe
跟 []
而言,我们可以把 <*>
想做是从左边的参数取出一个函数,然后套用到右边的参数上。至于 IO
,这种取出的模拟方式仍然适用,但我们必须多加一个 sequencing 的概念,因为我们是从两个 I/O action 中取值,也是在 sequencing,把他们黏成一个。我们从第一个 I/O action 中取值,但要取出 I/O action 的结果,他必须要先被执行过。getLine
黏在一起,然后用一个 return
,这是因为我们想要这个黏成的 I/O action 包含 a ++ b
的结果。我们也可以用 applicative style 的方式来描述:getLine
的型别是 getLine :: IO String
。当我们对 applicative functor 使用 <*>
的时候,结果也会是 applicative functor。getLine
想做是一个去真实世界中拿取字串的盒子。而 (++) <gt; getLine <*> getLine
会创造一个比较大的盒子,这个大盒子会派两个盒子去终端拿取字串,并把结果串接起来放进自己的盒子中。(++) <gt; getLine <*> getLine
的型别是 IO String
,他代表这个表达式式一个再普通不过的 I/O action,他里面也装着某种值。这也是为什么我们可以这样写:return
来将结果包起来。 那你可以考虑使用 applicative style,这样可以更简洁。Applicative
的 instance 是 (->) r
。虽然他们通常是用在 code golf 的情况,但他们还是十分有趣的例子。所以我们还是来看一下他们是怎么被实作的。pure
将一个值包成 applicative functor 的时候,他产生的结果永远都会是那个值。也就是最小的 context。那也是为什么对于 function 的 pure
实作来讲,他就是接受一个值,然后造一个函数永远回传那个值,不管他被喂了什么参数。如果你限定 pure
的型别至 (->) r
上,他就会是 pure :: a -> (r -> a)
。<*>
的实作是比较不容易了解的,我们最好看一下怎么用 applicative style 的方式来使用作为 applicative functor 的 function。<*>
可以产生一个新的 applicative functor,所以如果我们丢给他两个函数,我们能得到一个新的函数。所以是怎么一回事呢?当我们做 (+) <gt; (+3) <*> (*100)
,我们是在实作一个函数,他会将 (+3)
跟 (*100)
的结果再套用 +
。要看一个实际的范例的话,可以看一下 (+) <gt; (+3) <*> (*100) $ 5
首先 5
被丢给 (+3)
跟 (*100)
,产生 8
跟 500
。然后 +
被套用到 8
跟 500
,得到 508
。\x y z -> [x,y,z]
,而丢的参数是 (+3)
, (*2)
跟 (/2)
。5
被丢给以上三个函数,然后他们结果又接到 \x y z -> [x, y, z]
。k <gt; f <*> g
会制造一个函数,他会将 f
跟 g
的结果丢给 k
。当我们做 (+) <gt; Just 3 <*> Just 5
,我们是用 +
套用在一些可能有或可能没有的值上,所以结果也会是可能有或没有。当我们做 (+) <gt; (+10) <*> (+5)
,我们是将 +
套用在 (+10)
跟 (+5)
的结果上,而结果也会是一个函数,当被喂给一个参数的时候会产生结果。(->) r
怎么定义成 Applicative
的并不是真的那么重要,所以如果你不是很懂的话也没关系。这只是让你获得一些操作上的直觉罢了。Applicative
的 instance 是 ZipList
,他是包含在 Control.Applicative
中。<*>
,左边是许多函数,而右边是许多值,那结果会是函数套用到值的所有组合。如果我们做 [(+3),(*2)] <*> [1,2]
。那 (+3)
会先套用至 1
跟 2
。接着 (*2)
套用至 1
跟 2
。而得到 [4,5,2,4]
。[(+3),(*2)] <*> [1,2]
也可以这样运作:把左边第一个函数套用至右边第一个值,接着左边第二个函数套用右边第二个值,以此类推。这样得到的会是 [4,4]
。或是 [1 + 3, 2 * 2]
。ZipList a
,他只有一个构造子 ZipList
,他只包含一个字段,他的型别是 list。<*>
做的就是我们之前说的。他将第一个函数套用至第一个值,第二个函数套用第二个值。这也是 zipWith (\f x -> f x) fs xs
做的事。由于 zipWith
的特性,所以结果会跟 list 中比较短的那个一样长。pure
也值得我们讨论一下。他接受一个值,把他重复地放进一个 list 中。pure "haha"
就会是 ZipList (["haha","haha","haha"...
。这可能会造成些混淆,毕竟我们说过 pure
是把一个值放进一个最小的 context 中。而你会想说无限长的 list 不可能会是一个最小的 context。但对于 zip list 来说这是很合理的,因为他必须在 list 的每个位置都有值。这也遵守了 pure f <*> xs
必须要等价于 fmap f xs
的特性。如果 pure 3
只是回传 ZipList [3]
,那 pure (*2) <*> ZipList [1,5,10]
就只会算出 ZipList [2]
,因为两个 zip list 算出结果的长度会是比较短的那个的长度。如果我们 zip 一个有限长的 list 以及一个无限长的 list,那结果的长会是有限长的 list 的长度。ZipList a
型别并没有定义成 Show
的 instance,所以我们必须用 getZipList
函数来从 zip list 取出一个普通的 list。zipWith
,标准函式库中也有 zipWith3
, zipWith4
之类的函数,最多支持到 7。zipWith
接受一个接受两个参数的函数,并把两个 list zip 起来。zipWith3
则接受一个接受三个参数的函数,然后把三个 list zip 起来。以此类推。用 applicative style 的方式来操作 zip list 的话,我们就不需要对每个数量的 list 都定义一个独立的 zip 函数来 zip 他们。我们只需要用 applicative style 的方式来把任意数量的 list zip 起来就可以了。Control.Applicative
定义了一个函数叫做 liftA2
,他的型别是 liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c
。他定义如下:(a -> b -> c) -> (f a -> f b -> f c)
。当我们从这样的角度来看他的话,我们可以说 liftA2
接受一个普通的二元函数,并将他升级成一个函数可以运作在两个 functor 之上。Just 3
跟 Just 4
。我们假设后者是一个只包含单元素的 list。Just 3
跟 Just [4]
。我们有怎么得到 Just [3,4]
呢?很简单。:
是一个函数,他接受一个元素跟一个 list,并回传一个新的 list,其中那个元素已经接在前面。现在我们有了 Just [3,4]
,我们能够将他跟 Just 2
绑在一起变成 Just [2,3,4]
吗?当然可以。我们可以将任意数量的 applicative 绑在一起变成一个 applicative,里面包含一个装有结果的 list。我们试着实作一个函数,他接受一串装有 applicative 的 list,然后回传一个 applicative 里面有一个装有结果的 list。我们称呼他为 sequenceA
。x
是一个 applicative 而 xs
是一串 applicatve),我们可以对尾巴调用 sequenceA
,便会得到一个装有 list 的 applicative。然后我们只要将在 x
中的值把他接到装有 list 的 applicative 前面就可以了。sequenceA [Just 1, Just 2]
,也就是 (:) <gt; Just 1 <*> sequenceA [Just 2]
。那会等价于 (:) <gt; Just 1 <*> ((:) <gt; Just 2 <*> sequenceA [])
。我们知道 sequenceA []
算出来会是 Just []
,所以运算式就变成 (:) <gt; Just 1 <*> ((:) <gt; Just 2 <*> Just [])
,也就是 (:) <gt; Just 1 <*> Just [2]
,算出来就是 Just [1,2]
。sequenceA
的方式是用 fold。要记得几乎任何需要走遍整个 list 并 accumulate 成一个结果的都可以用 fold 来实作。pure []
。我们是用 liftA2 (:)
来结合 accumulator 跟 list 中最后的元素,而得到一个 applicative,里面装有一个单一元素的一个 list。然后我们再用 liftA2 (:)
来结合 accumulator 跟最后一个元素,直到我们只剩下 accumulator 为止,而得到一个 applicative,里面装有所有结果。Maybe
上时,sequenceA
创造一个新的 Maybe
,他包含了一个 list 装有所有结果。如果其中一个值是 Nothing
,那整个结果就会是 Nothing
。如果你有一串 Maybe
型别的值,但你只在乎当结果不包含任何 Nothing
的情况,这样的特性就很方便。sequenceA
接受装有一堆函数的 list,并回传一个回传 list 的函数。在我们的范例中,我们写了一个函数,他只接受一个数值作为参数,他会把他套用至 list 中的每一个函数,并回传一个包含结果的 list。sequenceA [(+3),(+2),(+1)] 3
会将 3
喂给 (+3)
, (+2)
跟 (+1)
,然后将所有结果装在一个 list 中。(+) <gt; (+3) <*> (*2)
会创见一个接受单一参数的一函数,将他同时喂给 (+3)
跟 (*2)
,然后调用 +
来将两者加起来。同样的道理,sequenceA [(+3),(*2)]
是制造一个接受单一参数的函数,他会将他喂给所有包含在 list 中的函数。但他最后不是调用 +
,而是调用 :
跟 pure []
来把结果接成一个 list,得到最后的结果。sequenceA
非常好用。例如说,我们手上有一个数值,但不知道他是否满足一串 predicate。一种实作的方式是像这样:and
接受一串布尔值,并只有在全部都是 True
的时候才回传 True
。 另一种实作方式是用 sequenceA
:sequenceA [(>4),(<10),odd]
接受一个函数,他接受一个数值并将他喂给所有的 predicate,包含 [(>4),(<10),odd]
。然后回传一串布尔值。他将一个型别为 (Num a) => [a -> Bool]
的 list 变成一个型别为 (Num a) => a -> [Bool]
的函数,很酷吧。[ord, (+3)]
这样的 list,因为 ord
接受一个字符并回传一个数值,然而 (+3)
接受一个数值并回传一个数值。[]
一起使用的时候,sequenceA
接受一串 list,并回传另一串 list。他实际上是创建一个包含所有可能组合的 list。为了方便说明,我们比较一下使用 sequenceA
跟 list comprehension 的差异:sequenceA [[1,2],[3,4]]
。要知道这是怎么回事,我们首先用 sequenceA
的定义 sequenceA (x:xs) = (:) <gt; x <*> sequenceA xs
还有边界条件 sequenceA [] = pure []
来看看。你不需要实际计算,但他可以帮助你理解 sequenceA
是怎么运作在一串 list 上,毕竟这有点复杂。(+) <gt; [1,2] <*> [4,5,6]
会得到一个 non-deterministic 的结果 x + y
,其中 x
代表 [1,2]
中的每一个值,而 y
代表 [4,5,6]
中的每一个值。我们用 list 来表示每一种可能的情形。同样的,当我们在做 sequence [[1,2],[3,4],[5,6],[7,8]]
,他的结果会是 non-deterministic 的 [x,y,z,w]
,其中 x
代表 [1,2]
中的每一个值,而 y
代表 [3,4]
中的每一个值。以此类推。我们用 list 代表 non-deterministic 的计算,每一个元素都是一个可能的情形。这也是为什么会用到 list of list。sequenceA
跟 sequence
是等价的。他接受一串 I/O action 并回传一个 I/O action,这个 I/O action 会计算 list 中的每一个 I/O action,并把结果放在一个 list 中。要将型别为 [IO a]
的值转换成 IO [a]
的值,也就是会产生一串 list 的一个 I/O action,那这些 I/O action 必须要一个一个地被计算,毕竟对于这些 I/O action 你没办法不计算就得到结果。