Haskell 零碎备忘、基础加强、好的讲解

一、(?y) 这种匿名函数

greaterThan100 :: [Integer] -> [Integer]
greaterThan100 xs = filter (>100) xs

(>100) is an operator section: if ? is an operator, then (?y) is equivalent to the function \x -> x ? y, and (y?) is equivalent to \x -> y ? x. In other words, using an operator section allows us to partially apply an operator to one of its two arguments. What we get is a function of a single argument.

二、Function composition

都知道,点操作符(.)是用来 compose 两个函数的,但是为什么要引入它吗?仅仅是为了书写方便的语法糖?

foo :: (b -> c) -> (a -> b) -> (a -> c)
foo f g = \x -> f (g x)

Does foo actually do anything useful or was that just a silly exercise in working with types?
As it turns out, foo is really called (.), and represents function composition. That is, if f and g are functions, then f . g is the function which does first g and then f.

由此可见,并不仅仅是语法糖。

myTest :: [Integer] -> Bool
myTest xs = even (length (greaterThan100 xs))

myTest' :: [Integer] -> Bool
myTest' = even . length . greaterThan100

This example also demonstrates why function composition seems “backwards”: it’s because function application is backwards! Since we read from left to right, it would make sense to think of values as also flowing from left to right. But in that case we should write (x)f to denote giving the value x as an input to the function f. But no thanks to Alexis Claude Clairaut and Euler, we have been stuck with the backwards notation since 1734.

这里解释了读起来不是从左向右而是 “backwards”。因为一般习惯了函数写成 f(x) ,而不是写成 (x)f。函数式编程语言跟数学关系密切,所以这里可以理解成是为了照顾到数学的习惯。

三、“point-free” style

This style of coding in which we define a function without reference to its arguments — in some sense saying what a function is rather than what it does — is known as “point-free” style.

四、用中缀表达式理解 fold

fold :: b -> (a -> b -> b) -> [a] -> b
fold z f []     = z
fold z f (x:xs) = f x (fold z f xs)

fold f z [a,b,c] == a `f` (b `f` (c `f` z)) // 用中缀表达式理解

五、Haskell type class和 Java interface 的对比

Type classes in Haskell are used to present an interface for types that have some behavior in common.
Java interface 就不解释了。

相同点:Both define a set of types/classes which implement a specified list of operations.

不同点:
1)定义 Java 类时,必须声明它实现的任何接口。 类型类实例与相应类型的声明可以分开,甚至可以放在单独的模块中。

2)Java 只能动态单派发,而 Haskell 可以轻松实现动态多派发,例如,

class Blerg a b where
  blerg :: a -> b -> Bool

编译器应该选择哪种 Blerg 实现同时取决于类型 a 和 b,这是多派发。Java 编译器只支持动态单派发。

3)对于子类的二元相关操作,Java 较为笨拙,而 Haskell 可以很优雅,例如,

class Num a where
  (+) :: a -> a -> a
  ...

类似这种操作在Java中没有很好的方法,原因是,

  1. 二元操作的两个参数中的一个必须是 “privileged”,即实际上是在它上面调用(+)方法(假设 Java 支持运算符重载),这种不对称性很丑;
  2. 由于Java的子类型获取某个接口类型的两个参数并不能保证它们实际上是相同的类型,这使得实现二元运算符如(+)通常需要一些运行时类型检查。

六、side-effect、惰性求值、严格求值

严格求值的定义:Under a strict evaluation strategy, function arguments are completely evaluated before passing them to the function.
严格求值的好处:The benefit of strict evaluation is that it is easy to predict when and in what order things will happen. Usually languages with strict evaluation will even specify the order in which function arguments should be evaluated (e.g. from left to right).

side-effect 的定义:By “side effect” we mean anything that causes evaluation of an expression to interact with something outside itself.

彩蛋:The challenge facing the Haskell designers was to come up with a way to allow such effects in a principled, restricted way that did not interfere with the essential purity of the language. They finally did come up with something (namely, the IO monad).

惰性求值的定义:Under a lazy evaluation strategy, evaluation of function arguments is delayed as long as possible: they are not evaluated until it actually becomes necessary to do so.

When some expression is given as an argument to a function, it is simply packaged up as an unevaluated expression (called a “thunk”, don’t ask me why) without doing any actual work.

惰性求值的时机:The slogan to remember is “pattern matching drives evaluation”. To reiterate the important points:

  • Expressions are only evaluated when pattern-matched
  • …only as far as necessary for the match to proceed, and no farther!
  1. “模式匹配驱动求值”
  2. Important thing to note is that thunks are evaluated only enough to allow a pattern match to proceed, and no further! (求值时求到够用即可)

七、关于 fold 一段很精彩的话

The fold for T will take one (higher-order) argument for each of T’s constructors, encoding how to turn the values stored by that constructor into a value of the result type — assuming that any recursive occurrences of T have already been folded into a result. Many functions we might want to write on T will end up being expressible as simple folds.

八、关于 Haskell 的 IO,一段精彩的话

Values of type IO a are descriptions of effectful computations, which, if executed would (possibly) perform some effectful I/O operations and (eventually) produce a value of type a. A value of type IO a, in and of itself, is just an inert, perfectly safe thing with no effects. It is just a description of an effectful computation.

In the same way, a value of type IO a is just a “recipe” for producing a value of type a (and possibly having some effects along the way). Like any other value, it can be passed as an argument, returned as the output of a function, stored in a data structure, or (as we will see shortly) combined with other IO values into more complex recipes.

So, how do values of type IO a actually ever get executed? There is only one way: the Haskell compiler looks for a special value.

Think of the Haskell runtime system as a master chef who is the only one allowed to do any cooking. If you want your recipe to be followed then you had better make it part of the big recipe (main) that gets handed to the master chef. Of course, main can be arbitrarily complicated, and will usually be composed of many smaller IO computations.

九、IO String 和 String 的区别(也适用于 IO a 和 a)

A value of type IO String is a description of some computation, a recipe, for generating a String. There is no String “inside” an IO String, any more than there is a cake “inside” a cake recipe. To produce a String (or a delicious cake) requires actually executing the computation (or recipe). And the only way to do that is to give it (perhaps as part of some larger IO value) to the Haskell runtime system, via main.

注意第一句话:A value of type IO String is a description of some computation, a recipe, for generating a String. (只是一个描述而已。)

十、IO 的简单组合

The simplest way to combine two IO computations is with the (>>) operator (pronounced “and then”), which has the type

(>>) :: IO a -> IO b -> IO b

This simply creates an IO computation which consists of running the two input computations in sequence. Notice that the result of the first computation is discarded; we only care about it for its effects. For example:

main = putStrLn "Hello" >> putStrLn "world!"

What if we don’t want to throw away the result from the first computation?
there is an operator (>>=) (pronounced “bind”) with the type

(>>=) :: IO a -> (a -> IO b) -> IO b

十一、type 和 Kind,以及深入理解 Kind

Types are little labels that values carry so that we can reason about the values. But types have their own little labels, called kinds. A kind is more or less the type of a type.

类型是值的标签,而 Kind 是类型的标签。
弄清楚三个东西:值、类型、Kind

搞懂 kind,能加深对 Haskell 类型系统的理解。
在 ghci 中,通过 :k 查看 kind,

ghci> :k Int
Int :: *

A * means that the type is a concrete type. A concrete type is a type that doesn't take any type parameters and values can only have types that are concrete types.

一个 * 代表这个类型是 concrete type。一个 concrete type 是没有任何类型参数,且值只能属于 concrete type

再看看 Maybe 的 kind,

ghci> :k Maybe  
Maybe :: * -> * 

Maybe is, in a sense, a function on types — we usually call it a type constructor.

The Maybe type constructor takes one concrete type (like Int) and then returns a concrete type like Maybe Int. And that's what this kind tells us. Just like Int -> Int means that a function takes an Int and returns an Int, * -> * means that the type constructor takes one concrete type and returns a concrete type.

Maybe 的类型构造子接受一个具体类型(如 Int)然后回传一个具体类型(如 Maybe Int)。这就是 kind 告诉我们的信息。就像 Int -> Int 代表这个函数接受一个 Int 并返回一个 Int* -> * 代表这个类型构造子接受一个具体类型并返回一个具体类型。

再看看 Either,

ghci> :k Either 
Either :: * -> * -> *

这说明 Either 接受两个具体类型作为参数,并构造出一个具体类型。它看起来也像是一个接受两个参数并返回值的函数类型。类型构造子是可以 curry 化的,所以也能 partially apply,

ghci> :k Either String  
Either String :: * -> *  
ghci> :k Either String Int  
Either String Int :: *  

再来看看 Functor 的定义,

class Functor f where   
    fmap :: (a -> b) -> f a -> f b  

可以看到 f 类型变量是接受一个 concrete type 且构造出一个 concrete type 的类型。 知道它构造出 concrete type 是因为是作为函数参数的类型。 从那里可以推测出一个类型要是属于 Functor 必须是 * -> * Kind。

接下来看看下面这个新定义的 typeclass,看看 Kind 的威力,

class Xxxxx t where  
    yyyyy :: j a -> t a j

可以来看看他的 kind 是什么?

  1. 由于 j a 被当作 yyyyy 这个函数的入参,所以 j a 的 Kind 一定是 *
  2. 假设 a 的 Kind 是 *,那么 j 的 Kind 就会是 * -> *
  3. 由于 t a j 是函数的返回值,而知道 aj 的 Kind 分别是 ** -> *,因此可以推测出 t 的 Kind 是 * -> (* -> *) -> *
  4. 也就是说 t 接受一个具体类型 a、一个接受单一参数的类型构造子 j,然后产生出一个具体类型;
  5. Xxxxx is a higher-order type constructor, in the same way that map is a higher-order function. (注意对比理解)

看最后一个例子,定义一个类型具有 * -> (* -> *) -> * 的 kind,下面是一种定义的方法,

data Frank a b  = Frank {frankField :: b a} deriving (Show)  

怎么推断出这个类型具有 * -> (* -> *) -> * 的 kind 呢?

  1. ADT 中的字段是要来塞值的,所以其 Kind 必须是 *
  2. 假设 a*,那 b 就是接受一个类型参数的 Kind,即 * -> *
  3. 现在知道 a 和 b 的 Kind 了,而它们又是 Frank 的类型参数,所以可以推断出 Frank 的 Kind 是 * -> (* -> *) -> *,其中第一个 * 代表 a,第二个 (* -> *) 代表 b。

十二、 为什么要引入 Functor

这里尝试换个角度说明为什么要引入 Functor。

从消除重复代码的角度

首先明确一点,软件设计的很多概念和思想其实都是为了一件事情 ------ 消除重复代码,例如继承。

因此从这个角度来看,Functor 也是一种消除重复代码的方式。先看看一堆的 Map 函数,

map :: (a -> b) -> [a] -> [b]

treeMap :: (a -> b) -> Tree a -> Tree b

maybeMap :: (a -> b) -> Maybe a -> Maybe b

仔细对比发现,这是 repeated pattern,因此需要消除,将类型抽象出来,就可以消除重复代码了,

thingMap :: (a -> b) -> f a -> f b

thingMap has to work differently for each particular f.

可以看到,这和 fmap 的签名是一样的。

从设计模式的角度

对于 OO 类的静态语言如 C++、Java等,设计模式用得非常多。
设计模式用一句简单的话来总结就是 ------ 用固定的“套路”解决问题。这些“套路”就是那些设计模式了。

在 FP 类的语言中,也有不少“套路”,例如 Functor/Applicative/Monad 等。

彩蛋

来玩一个好玩的事情。
回顾一下,Functor 定义如下,

class Functor f where
  fmap :: (a -> b) -> f a -> f b

如果对 (->) e 实现 Functor 会怎样?将其代入 Functor 的定义中,得到,

instance Functor ((->) e) where
  fmap :: (a -> b) -> (->) e a -> (->) e b

然后将 (->) 写成中缀写法,

instance Functor ((->) e) where
  fmap :: (a -> b) -> (e -> a) -> (e -> b)

仔细看,上面这个函数签名是不是很熟悉?是的,

instance Functor ((->) e) where
  fmap = (.)

Crazy!
Funny!

十三、Applicator 的一些杂记

Like pure (+) <*> Just 3 <*> Just 5(操作结果为 Just 8), applicative functors and the applicative style of doing pure f <*> x <*> y <*> ... allow us to take a function that expects parameters that aren't necessarily wrapped in functors(pure 函数可以将该 function 包装到 functors 中) and use that function to operate on several values that are in functor contexts(通过 <*> 函数). The function can take as many parameters as we want, because it's always partially applied step by step between occurences of <*>.

十四、有了 Applicator 为何还需要 Monad

Applicative gives us no way to decide what to do next based on previous results: we must decide in advance operations we are going to run, before we see the results.

十五、Type variables are independent of parameter names or other value names

例子,

(<$>) :: (Functor f) => (a -> b) -> f a -> f b  
f <$> x = fmap f x 

The f in the function declaration here is a type variable with a class constraint saying that any type constructor that replaces f should be in the Functor typeclass. The f in the function body denotes a function that we map over x. The fact that we used f to represent both of those doesn't mean that they somehow represent the same thing.

十六、sequence 函数来历及其用法举例

怎么来

We can combine any amount of applicatives into one applicative that has a list of the results of those applicatives inside it,

sequenceA :: (Applicative f) => [f a] -> f [a]  
sequenceA [] = pure []  
sequenceA (x:xs) = (:) <$> x <*> sequenceA xs 

例如,combine Just [3,4] with Just 2 to produce Just [2,3,4]

Another way to implement sequenceA is with a fold. Remember, pretty much any function where we go over a list element by element and accumulate a result along the way can be implemented with a fold,

sequenceA :: (Applicative f) => [f a] -> f [a]  
sequenceA = foldr (liftA2 (:)) (pure [])  

其中,liftA2 定义如下,

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

换个角度理解 sequenceA ------ Doing (+) <$> (+3) <*> (*2) will create a function that takes a parameter, feeds it to both (+3) and (*2) and then calls + with those two results. In the same vein, it makes sense that sequenceA [(+3),(*2)] makes a function that takes a parameter and feeds it to all of the functions in the list. Instead of calling + with the results of the functions, a combination of : and pure [] is used to gather those results in a list, which is the result of that function.

怎么用

示例一

Using sequenceA is cool when we have a list of functions and we want to feed the same input to all of them and then view the list of results. For instance,

ghci> map (\f -> f 7) [(>4),(<10),odd]  
[True,True,True]  
ghci> and $ map (\f -> f 7) [(>4),(<10),odd]  
True  

Another way to achieve the same thing would be with sequenceA,

ghci> sequenceA [(>4),(<10),odd] 7  
[True,True,True]  
ghci> and $ sequenceA [(>4),(<10),odd] 7  
True  

注意:lists are homogenous, all the functions in the list have to be functions of the same type, of course. You can't have a list like [ord, (+3)], because ord takes a character and returns a number, whereas (+3) takes a number and returns a number.

示例二

sequenceA 还可以用来做列表生成式(list comprehension),

ghci> sequenceA [[1,2,3],[4,5,6]]  
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]  
ghci> [[x,y] | x <- [1,2,3], y <- [4,5,6]]  
[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]  

ghci> sequenceA [[1,2],[3,4]]  
[[1,3],[1,4],[2,3],[2,4]]  
ghci> [[x,y] | x <- [1,2], y <- [3,4]]  
[[1,3],[1,4],[2,3],[2,4]]  

Here is how sequenceA works,

  1. We start off with sequenceA [[1,2],[3,4]]
  2. That evaluates to (:) <$> [1,2] <*> sequenceA [[3,4]]
  3. Evaluating the inner sequenceA further, we get (:) <$> [1,2] <*> ((:) <$> [3,4] <*> sequenceA [])
  4. We've reached the edge condition, so this is now (:) <$> [1,2] <*> ((:) <$> [3,4] <*> [[]])
  5. Now, we evaluate the (:) <$> [3,4] <*> [[]] part, which will use : with every possible value in the left list (possible values are 3 and 4) with every possible value on the right list (only possible value is []), which results in [3:[], 4:[]], which is [[3],[4]]. So now we have (:) <$> [1,2] <*> [[3],[4]]
  6. Now, : is used with every possible value from the left list (1 and 2) with every possible value in the right list ([3] and [4]), which results in [1:[3], 1:[4], 2:[3], 2:[4]], which is [[1,3],[1,4],[2,3],[2,4]
示例三

When used with I/O actions, sequenceA is the same thing as sequence! It takes a list of I/O actions and returns an I/O action that will perform each of those actions and have as its result a list of the results of those I/O actions.
That's because to turn an [IO a] value into an IO [a] value, to make an I/O action that yields a list of results when performed, all those I/O actions have to be sequenced so that they're then performed one after the other when evaluation is forced. Remenber that You can't get the result of an I/O action without performing it.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,106评论 0 10
  • 之前觉得工作挺好玩的,不管是契调还是理赔,都会遇到形形色色的人。不过那个时候没有四种象限的概念,我就觉得欠缺一些东...
    吾宗老孙子阅读 332评论 0 0
  • 年年不过重雪, 思故人远去, 念来者迷途, 青丝褪成白发, 旧事茵茵。
    许盛然阅读 721评论 0 0
  • 心血来潮给自己写点东西。 今天是2015最后一天,明天也是我十八岁生日,过了今晚零点,我就是个成年人了。 虽然往年...
    待锦世繁华阅读 929评论 0 0