Haskell

1,043 阅读22分钟

掘金都要变成的笔记备份战了,什么都在往上搬吧。很多东西太杂了,有些时候电脑不在身边看不了,就放上来。Haskell 趣学指南的摘抄算是,可以直接去看原文

常用操作: Haskell 的文件使用 .hs 结尾。Haskell 提供一个运行环境 - ghci,使用命令 ghci 启用,可以进行简单的交互,也可以使用命令 :l fileName.hs,加载 Haskell 文件,ghc 是编译 .hs 文件的命令。

基础

Haskell中函数有最高的优先级,并且函数调用传参可以不用小括号,并且有前缀和中缀的调用方式。

succ 8 --succ是一个函数,获得参数的后继,如8 -> 9 函数名加空格调用,也可以加小括号
succ 9 + max 10 12 + 1
-- 相当于
(succ 9) + (max 10 12) + 1
-- div 除法函数,下面是前缀调用
div 90 10
-- 也可以使用下面的中缀调用, 中缀调用要将函数名使用反引号(`)包裹
90 `div` 10

Haskell有条件语句,但是 else 不能省略

doubleSmallNumber x =
  if x > 100
    then x
    else x * 2

Haskell中有列表,列表是一种单类型(homogeneous)的数据结构,只能存放多个单一类型的数据,不能数字和字符放一起。

someNumbers = [1, 2, 3, 4]

使用 ++ 拼接两个列表,同时Haskell中没有字符串,字符串相当于一组字符组成的列表, "Tom" 等效于 ["T", "o", "m"],++运算符是遍历第一个列表的(也就是++左边的列表),如果数据太大会很慢。

[1, 2] ++ [3, 4] -- expected [1, 2, 3, 4]
"hello" ++ " " ++ "world"-- expected "hello world"

:运算符,可以将一个元素插入到列表表头。

5:[1, 2] -- excepted [5, 1, 2]
-- 需要注意,实际上 [1, 2, 3] 是 1: 2 :3 :[]的语法糖

如果需要获取列表中的某一个元素,可以使用 !! 运算符,相当于数组下标。

"Steve Buscemi" !! 6 --excepted "B"

类型

Haskell 是强类型语言,基本运算可以通过参数推断出来,也可以手动指定类型,非常类似 TS,但是 Haskell 的类型声明更加强大。

removeNonUppercase :: [Char] -> [Char]  -- 这是 removeNonUppercase 的函数声明,输入 [Char],输出也是 [Char] 
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']] -- 下面是具体实现

下面是一个多参数函数的声明示例,可以看到 Haskell 中并没有像 TS 一样,将传参使用括号包裹起来,如 (a:number, b:number, c:number) => number

Haskell 是函数式编程语言,必然返回一个值。这样除了最后一个 Int,其他的都是参数的类型约束。更主要的原因是,Haskell 中的函数都是 Curried Function。标准的函数式编程,都是接受一个值,返回一个值,函数也可以看作一个值。

:t 命令,可以返回相应的类型。

addThree :: Int -> Int -> Int -> Int  
addThree x y z = x + y + z

Haskell 中也存在类似泛型(generic) 的概念,叫做类型变量(Type variables)。和 TS 不同,Haskell 中通常使用小写 a,b,c,d... 来表示泛型。

如内置的 head 函数,用来获取 list 的第一个元素,list 中放置的元素类型是不确定的,所以这里使用 a 来表示一个不定的类型。

:t head  
head :: [a] -> a

使用类型变量,可以是函数的类型声明更加强大,可以写出通用性更高的,与类型无关的函数,使用到类型变量的函数又叫多态函数

Typeclass

型别定义行为的接口,如果一个型别属于某 Typeclass,那它必实现了该 Typeclass 所描述的行为。

:t (==)  
(==) :: (Eq a) => a -> a -> Bool

在这里我们见到个新东西:=> 符号。它左边的部分叫做型别约束。我们可以这样阅读这段型别声明:"相等函数取两个相同型别的值作为参数并回传一个布尔值,而这两个参数的型别同在 Eq 类之中(即型别约束)"。

可以简单理解为,Eq 是一个泛型的约束条件,这里我们需要用到一个泛型 a, Eq 约束了 a 范围,只要能使用 == 判断是否相等的类型,都是属于 Eq。

函数

模式匹配 (Pattern matching)

下面是一个模式匹配的例子

sayMe :: (Integral a) => a -> String  
sayMe 1 = "One!"  
sayMe 2 = "Two!"  
sayMe 3 = "Three!"  
sayMe 4 = "Four!"  
sayMe 5 = "Five!"  
sayMe x = "Not between 1 and 5"

sayMe 从上到下进行匹配,如果匹配到就执行,并忽略下面的模式,最后的 x 代表一个匹配任何情况的默认执行,没有默认执行,如果没有匹配到,就会报错。

这样来看,模式匹配和 switch 语句好像区别不大,看起来平平无奇,下面来看一个更厉害的例子。

如果现在需要实现一个二维矢量相加的函数(x, y 分别相加),最基础的写法可能是这样。

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors a b = (fst a + fst b, snd a + snd b)

如果使用模式匹配

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)  
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

非常简单,而且清晰,(x1, y1) (x2, y2) 类似两个元组的泛型,返回一个元组 (x1 + x2, y1 + y2)。

接下来是一个 list 的求和计算函数

sum' :: (Num a) => [a] -> a  
sum' [] = 0  
sum' (x:xs) = x + sum' xs

如果是空 list 直接返回 0。

list 又可以看作 head: tail,将头部的第一个元素,插入到尾部的 list 前。第二个模式的意思就是,list a 的头部是 x,剩下的 list 尾部就是 xs,这样可以通过模式匹配简单的将头部 x 取出来,再递归获取尾部的 sum 结果,并将两者加起来。

Guard

模式匹配虽然方便,但是只能匹配特定值。

例如模式匹配的第一个例子,只能 1,2,3,4,5 一个个地匹配,如果是 1 - 10,统一匹配一个结果,剩下的走默认处理,这样就需要写很多重复的代码。

这时候就会用 Guard,和模式匹配不同,Guard 不匹配特定条件,而是匹配一个 Boolean 表达式,类似 if 。

下面是一个计算 BMI,依据你的 BMI 值 (body mass index,身体质量指数)来不同程度地侮辱你的函数。BMI 值即为体重除以身高的平方。如果小于 18.5,就是太瘦;如果在 18.5 到 25 之间,就是正常;25 到 30 之间,超重;如果超过 30,肥胖。

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"  
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise                 = "You're a whale, congratulations!"

bmiTell 是函数名, weight height 是两个参数,注意这里和一般函数的区别。Guard 函数后面直接跟 Guard 的语法,没有使用 = ,将函数实现和名称,参数连接起来。|表示一个 Guard 语句的开始,后面跟条件判断,紧跟 = 返回值,otherwise 跟一个默认的处理。

上述代码中 weight / height ^ 2 被计算了三次,统一个函数体中,计算三次很是浪费,所以有了 where 的语法。

下面是一个使用 where 优化后的代码

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | bmi <= 18.5 = "You're underweight, you emo, you!"  
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!"  
    | otherwise   = "You're a whale, congratulations!"  
    where bmi = weight / height ^ 2

这里通过 where 提供了一个变量,通过一次计算,在当前函数语句中都可以使用,where 是可以定义多个变量或者函数的。

let 变量

通过 where 只能定义一个在函数内部可用的变量,当我们需要跨函数调用就很麻烦。

let 绑定则是个表达式,允许你在任何位置定义局部变量,let 的格式为 let [bindings] in [expressions]。在 let 中绑定的名字仅对 in 部分可见。

cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h = 
    let sideArea = 2 * pi * r * h  
        topArea = pi * r ^2  
    in  sideArea + 2 * topArea

递归

递归之于函数式编程,就像循环之于命令式编程。

理解这两者差别,可以更清楚地理解函数式编程和命令式编程。

首先从大家更熟悉的命令式编程说起

for(let i =0; i < 10; i++) {
  console.log(i)
}

上述是一个简单的 loop 结构。顾名思义,命令时编程,也就是发布命令,告诉程序每一步做什么,如例子中,声明一个 i ,告诉程序,如果 i < 10,就打印 i,并且将 i 加一,再次判断,直到不符合条件。

接下来看一个典型的递归函数

maximum' :: (Ord a) => [a] -> a  
maximum' [] = error "maximum of empty list"  
maximum' [x] = x  
maximum' (x:xs)   
    | x > maxTail = x  
    | otherwise = maxTail  
    where maxTail = maximum' xs

这里使用了模式匹配,首先空 list,会报错,单个元素的 list,返回 list 的第一个元素,也是唯一一个元素。当 list 中不止一个元素是,比较 list 的 head,与对 list 的 tail 递归调用 maximum' 结果,取两者中大的值作为结果。

函数式编程,区别于命令,更像一组描述。在这个过程中,我们既不知道每一步需要操作什么,不知道需要多少步。只是给了每一步可能需要用到的操作,已经针对不同操作的情景描述。根据这些描述,程序会自动匹配查找操作,并一步步找到结果。这就是函数式编程的魅力,它不会冷冰冰地告诉你需要怎么做。只会把所有的解决方式告诉你,等待你自己找出通往结果的那条路。

快速排序

quicksort :: (Ord a) => [a] -> [a]  
quicksort [] = []  
quicksort (x:xs) =  
  let smallerSorted = quicksort [a | a <- xs, a <= x] 
      biggerSorted = quicksort [a | a <- xs, a > x]  
  in smallerSorted ++ [x] ++ biggerSorted

第一步将 list 头部的元素,插入到两个 list 中间,smallerSorted 是小于等于 x 的元素组成的 list,biggerSorted 是比 x 大的元素组成的 list。接着再对 smallerSorted,biggerSorted 进行递归。

若给 [5,1,9,4,6,7,3] 排序,这个算法就会取出它的头部,即 5。 将其置于分别比它大和比它小的两个 List 中间,得 [1,4,3] ++ [5] ++ [9,6,7], 我们便知道了当排序结束之时,5会在第四位,因为有3个数比它小,也有三个数比它大。好的,接着排 [1,4,3][9,6,7], 结果就出来了!

Collatz 串行

Collatz 串行:取一个自然数,若为偶数就除以 2。 若为奇数就乘以 3 再加 1。 再用相同的方式处理所得的结果,得到一组数字构成的的链。它有个性质,无论任何以任何数字开始,最终的结果都会归 1。

chain :: (Integral a) => a -> [a]  
chain 1 = [1]
chain x
      | even x = x:chain (x`div`2)
      | odd x = x:chain (n*3 + 1)

模块

Haskell 中使用 import 装载模块,Prelude 模块是缺省自动加载的。在 Haskell中,装载模块必须得在函数的定义之前,所以一般都是将它置于代码的顶部。一段代码中可以装载很多模块,只要将 import 语句分行写开即可。

例如,import Data.List 这样一来 Data.List 中包含的所有函数就都进入了全局命名空间。也就是说,你可以在代码的任意位置调用这些函数.Data.List 模块中有个 nub 函数,它可以筛掉一个 List 中的所有重复元素。用点号将 lengthnub 组合: length . nub,即可得到一个与 (\xs -> length (nub xs)) 等价的函数。

import Data.List  
​
numUniques :: (Eq a) => [a] -> Int  
numUniques = length . nub

也可以只从模块中加载几个函数。

import Data.List (nub, sort) -- 这样就从 `Data.List` 加载了 nub,sort 两个函数

上述导入都是直接导入到全局命名空间的,如果模块中的函数与全局中已有的函数冲突,就需要去掉冲突的函数。

import Data.List hiding (nub) -- 这样就会不导入 nub,其余全部导入

也可以给新导入的模块增加命名空间:

import qualified Data.Map as M -- as 用来重命名,默认命名空间即模块名 Data.Map

Types 构造

Haskell 中允许用户自己定义类型,通过使用 data 关键字。

data Replays = "Thanks" | "Fuck off"

data 后面跟的是类型名,= 右边是用 |拼接起来的“枚举值”,还记得 Haskell 中最重要的特性嘛,什么都是函数。所以这里的 "Thanks" 和 "Fuck off",实际上就是函数,调用函数后就返回对应的值。Haskell 对这里的“枚举值”有特别的称呼,叫做值构造子。型别名和值构造子的首字母必大写。

既然是函数就可以传递多个参数,比如说我们要描述一个人,可能会用名字,年龄,性别这些维度,Haskell 中就可以这样描述:

data Sex = "Male" | "Female" -- 这里定义一个性别类型
data Person = Person String Int Sex

Person 类别中有一个值构造子,接受三个参数,类型分别是 String,Int,Sex(我们自己定义的,有两个可能 Male 和 Female)。对于一个类,需要有一些基本的方法,例如需要知道这个 Person 的 name。这样就需要定义一些基本方法:

name:: Person -> String
name (Person name _ _) = name -- 通过模式匹配获取 name,实际上 Haskell 中的模式匹配,匹配的就是值构造子!!

仔细想想,上面的写法会有些让人疑惑,看到 Person 只知道需要几个参数,并不清楚参数什么意思,如果维度很多,大概会看着一大堆的 String,Int... 发呆。

Haskell 也提供了一种更清晰的写法:

data Person = Person {
  name:: String,
  age:: Int,
  sex:: Sex
} deriving (Show) -- deriving 代表派生关系,例如 Show 就是将类型转化为 String,这样就可以打印到控制台

上面的写法不仅更清楚,Haskell 还对这种 Record Syntax 的写法有额外支持,针对类型中的每个属性, Haskell 会自动生成上面 name 类似的方法。

有时候一个数据是什么类型,可能会有多种情况,例如常见的 MaybeEither。可以参考TS中的 Promise,单纯的 Promise 不是一个完整的类型,需要确定 Promise 中返回的类型。

data Maybe a = Nothing | Just a
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)

类型定义中,也可以接受一个类别也可以接受一个参数(型别参数),有了型别参数,Maybe 就成为了一个型别构造子。Maybe 并不是一个完整的类型,通过它可以构造出 Maybe IntMaybe String 等诸多态别。

:t Just "Haha"
Just "Haha" :: Maybe [Char]

Maybe 只有成功情况才会传递数据,有时候判断成功与否都要做处理,就可以使用 Either。

一个例子:有个学校提供了不少壁橱,好给学生们地方放他们的 Gun'N'Rose 海报。每个壁橱都有个密码,哪个学生想用个壁橱,就告诉管理员壁橱的号码,管理员就会告诉他壁橱的密码。但如果这个壁橱已经让别人用了,管理员就不能告诉他密码了,得换一个壁橱。我们就用 Data.Map 的一个 Map 来表示这些壁橱,把一个号码映射到一个表示壁橱占用情况及密码的 Tuple 里。

import qualified Data.Map as Map
​
data LockerState = Taken | Free deriving (Show, Eq)
​
type Code = String -- 这里使用了 type 重命名了 String 类型,是为了方便阅读
​
type LockerMap = Map.Map Int (LockerState, Code)
​
lockerLookup :: Int -> LockerMap -> Either String Code
lockerLookup lockerNumber map =
    case Map.lookup lockerNumber map of  -- lookup 会返回一个 Maybe,成功代表已找到
        Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!"
        Just (state, code) -> if state /= Taken
                                then Right code
                                else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"

构造树

实现一个二叉搜索树 (binary search tree)。他的结构是每个节点指向两个其他节点,一个在左边一个在右边。在左边节点的元素会比这个节点的元素要小。在右边的话则比较大。每个节点最多可以有两棵子树。譬如说一棵包含 5 的节点的左子树,里面所有的元素都会小于 5。而节点的右子树里面的元素都会大于 5。如果我们想找找看 8是不是在我们的树里面,我们就从 5 那个节点找起,由于 8 比 5 要大,很自然地就会往右搜索。接着我们走到 7,又由于 8 比 7 要大,所以我们再往右走。我们在三步就找到了我们要的元素。如果这不是棵树而是 List 的话,那就会需要花到七步才能找到 8。

定义一个树的类型,不是一棵空的树就是带有值并含有两棵子树。

data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) -- Tree 是一个数据类型,这个类型递归调用了自身 

我们不太想手动来建棵二叉搜索树,实现一个函数来完成,他接受一棵树还有一个元素,把这个元素安插到这棵二叉搜索树中。当拿这个元素跟树的节点比较结果比较小的话,我们就往左走,如果比较大,就往右走。重复这个动作直到我们走到一棵空的树。一旦碰到空的树的话,我们就把元素插入节点。

singleton :: a -> Tree a -- 插入空树的操作
singleton x = Node x EmptyTree EmptyTree
​
treeInsert :: (Ord a) => a -> Tree a -> Tree a
treeInsert x EmptyTree = singleton x -- 如果是空树就插入
treeInsert x (Node a left right) -- left 是传入树的左子树,right 是右子树
      | x == a = Node x left right -- 如果存在直接返回原有的树,存在不需要插入
      | x < a  = Node a (treeInsert x left) right -- 小于就向左,递归查找左子树
      | x > a  = Node a left (treeInsert x right) -- 大于就向右,递归查找右子树

检查某个元素是否已经在这棵树中。判断和上面类似

treeElem :: (Ord a) => a -> Tree a -> Bool
treeElem x EmptyTree = False -- 空树一定不包含所要查找元素
treeElem x (Node a left right)
    | x == a = True -- 相等即查找到元素
    | x < a  = treeElem x left -- 小于向左递归查找
    | x > a  = treeElem x right -- 大于向右

测试

ghci> let nums = [8,6,4,1,7,3,5]
ghci> let numsTree = foldr treeInsert EmptyTree nums -- 从右向左 fold,所以 root 是 5
ghci> numsTree
Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree)) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree))  

instance typeclass

首先了解 Eq 这个 typeclass 的定义

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool
    x == y = not (x /= y)
    x /= y = not (x == y)

其中 a 是类型限定,也就是说只有统一类型的才可以进行 ==,/= 比较,这两种行为是互斥的。接下来定义一个 Star 类型,让它成为 Eq 的 instace。

data Star = Stella | Planet | Satellite
​
instance Eq Star where
    Stella == Stella = True
    Planet == Planet = True
    Satellite == Satellite = True
    _ == _ = False

使用模式匹配,定义 Eq 中 == 的行为。==,/= 是互斥的,在 Eq 中也是相互定义的,这里只需要定义一种行为就行。上面的 instance 过程可以直接使用 derive 实现,如果需要定义特殊的表现,可以手动 instance。

有时在定义 class 需要做一定的限制(class constraints),例如 Num 的定义就要求必须要是 Eq 类型。这样 Num 就是一个 subclass,定义 Num 也不需要重复实现 Eq 的行为。

class (Eq a) => Num a where
   ...

Maybe 类型和 Star 又有区别,因为 Maybe 不是一个完整的类型,所以 instance 时需要额外的参数

instance (Eq m) => Eq (Maybe m) where
  Just x == Just y = x == y
  Nothing == Nothing = True
  _ == _ = False

m 代表任意类型,这样 Maybe m 就是一个类型了,需要判断是否相等,所以 m 需要是 Eq 类型。可以看出来定义依旧很清晰,简直就和读文章一样,不需要太多的思考,Just x == Just y 和 x == y 结果相同,Nothing 相等,其余不相等。函数式编程就是这样,描述一个问题,而不是去指导你怎么做。

Functor

Functor 这个 typeclass,基本上就代表可以被 map over 的事物。听到这个词你可能会联想到 List,因为 map over list 在 Haskell 中是很常见的操作。

class Functor f where
    fmap :: (a -> b) -> f a -> f b -- f 是类型,a -> b 是对 f 中的数据进行映射,得到的还是一个映射后的 f 类型

Functor 中约束了一个 fmap 的行为,接受一个从一个型别映射到另一个型别的函数,还接受一个 functor 装有原始的型别(f a),然后会回传一个 functor 装有映射后的型别(f b)。可以看出来 f 是一个类别构造子,它接受一个参数,就像 Maybe 一样。

记得 map 的类型定义嘛: map :: (a -> b) -> [a] -> [b],这就是一个 fmap 的具体实现,List 也确实被定义成了 Functor 的 Instance。

instance Functor [] where
    fmap = map

要想理解 Functor,需要对比思考一下,为什么要有这个类型。在 Haskell 中,简单的函数接受参数返回值,例如 + 3 4,接受两个参数 Int,返回一个 Int 值,或者更近一步 (+ 3) 这个函数,接受一个 Int 值,返回一个 Int 值。

但是对于 List 和 Maybe 呢,我们不能直接运用 (+ 3) ,因为两个类型都不一样。差别在于 Int 是一个类型,而 List 和 Maybe 是一个带参数的类型,单独对 List 和 Maybe 做操作是没有意义的,也就是说直接操作缺少了一个参数。同时,数据结构是有意义的,对 List 和 Maybe 进行操作的时候,我们会希望继续保留这个结构。如果对一个 List 操作后,它会随机变成一种类型,想想你的代码怎么写(有点夸张,但是好理解这样做的原理)?

自然也会想到提供一个函数接受那个缺少的类型,就能保证对类型中的元素进行操作是没问题的,例如 map (+ 3) List::Int 。这个角度就能理解 Functor 的定义了,提供一个 fmap 函数,接受一个映射函数,然后对 Functor 的数据进行处理,并返回一个处理后的 Functor,如对 list 中所有元素进行 (+ 3)

ghci> :t fmap (*2)  
fmap (*2) :: (Num a, Functor f) => f a -> f a  
ghci> :t fmap (replicate 3)  
fmap (replicate 3) :: (Functor f) => f a -> f [a]

这里注意理解 ,函数式编程中的 Typeclass 和面向对象中的 Class 的区别,Typeclass 更像一个集合,只是范畴特性上的描述,也就意味着存在一个类型,即属于 Typeclass A 也属于 Typeclass B。面向对象的 class 则更实际,假设有一个 Dog 的 class,Dog 的实例不可能同时也是另一个无关的 class(比如,Cat) 的实例。比如这里的 fmap (replicate 3) 接受一个任何型态的 functor,最终返回一个数据类型是 list 的 functor,map 前后 functor 类型是一致的。

ghci> fmap (replicate 3) [1,2,3,4]  
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]  
ghci> fmap (replicate 3) (Just 4)  
Just [4,4,4]  
ghci> fmap (replicate 3) (Right "blah")  
Right ["blah","blah","blah"]  
ghci> fmap (replicate 3) Nothing  
Nothing  
ghci> fmap (replicate 3) (Left "foo")  
Left "foo"

与 群 有关的几个概念。

首先给定一个论域A(也就是一个集合,一个定义域),再给定一个二元运算 *。

  1. 若 *运算封闭,则成为 magma ( 原群)
  2. 若再 *运算满足结合律,则成为 semigroup (半群)
  3. 若再 存在单位元e,则成为 monoid(含幺半群)
  4. 若再 任意元素存在逆元,则成为 group (群)

\1. 若 *运算满足交换律,则成为 交换的,阿贝尔

半环、环

  1. 环:加法成群,乘法是半群,并且加上加法和乘法的compatible条件,
  2. 半环:加法是半群,乘法也是半群,加上compatible条件。
  3. 最简单的例子是自然数,加法和乘法就是正常理解的加法和乘法,这个可能就是半环的来源。

可以针对 Functor 改进的地方,例如 a -> b 也被包在一个 Functor value 里面呢?像是 Just (*3),我们要如何 apply Just 5 给他?如果我们不要 apply Just 5 而是 Nothing 呢?甚至给定 [(*2),(+4)],我们要如何 apply 他们到 [1,2,3] 呢?对于此,我们抽象出 Applicative typeclass,这就是我们想要问的问题:

(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b

我们也看到我们可以将一个正常的值包在一个数据型态中。例如说我们可以拿一个 1 然后把他包成 Just 1。或是把他包成 [1]。也可以是一个 I/O action 会产生一个 1。这样包装的 function 我们叫他做 pure

如我们说得,一个 applicative value 可以被看作一个有附加 context 的值。例如说,'a' 只是一个普通的字符,但 Just 'a' 是一个附加了 context 的字符。他不是 Char 而是 Maybe Char,这型态告诉我们这个值可能是一个字符,也可能什么都没有。

来看看 Applicative typeclass 怎样让我们用普通的 function 操作他们,同时还保有 context:

ghci> (*) <$> Just 2 <*> Just 8  
Just 16  
ghci> (++) <$> Just "klingon" <*> Nothing  
Nothing  
ghci> (-) <$> [3,4] <*> [1,2,3]  
[2,1,0,3,2,1]

所以我们可以视他们为 applicative values,Maybe a 代表可能会失败的 computation,[a] 代表同时有好多结果的 computation (non-deterministic computation),而 IO a 代表会有 side-effects 的 computation。

Monad 是一个从 Applicative functors 很自然的一个演进结果。对于他们我们主要考量的点是:如果你有一个具有 context 的值 m a,你能如何把他丢进一个只接受普通值 a 的函数中,并回传一个具有 context 的值?也就是说,你如何套用一个型态为 a -> m b 的函数至 m a?基本上,我们要求的函数是:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

如果我们有一个漂亮的值跟一个函数接受普通的值但回传漂亮的值,那我们要如何要把漂亮的值丢进函数中?这就是我们使用 Monad 时所要考量的事情。我们不写成 f a 而写成 m a 是因为 m 代表的是 Monad,但 monad 不过就是支持 >>= 操作的 applicative functors。>>= 我们称呼他为 bind。