一、(?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中没有很好的方法,原因是,
- 二元操作的两个参数中的一个必须是 “privileged”,即实际上是在它上面调用(+)方法(假设 Java 支持运算符重载),这种不对称性很丑;
- 由于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!
- “模式匹配驱动求值”
- 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 aString
. (只是一个描述而已。)
十、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 是什么?
- 由于
j a
被当作yyyyy
这个函数的入参,所以j a
的 Kind 一定是*
; - 假设
a
的 Kind 是*
,那么j
的 Kind 就会是* -> *
; - 由于
t a j
是函数的返回值,而知道a
和j
的 Kind 分别是*
和* -> *
,因此可以推测出t
的 Kind 是* -> (* -> *) -> *
; - 也就是说
t
接受一个具体类型a
、一个接受单一参数的类型构造子j
,然后产生出一个具体类型; -
Xxxxx
is a higher-order type constructor, in the same way thatmap
is a higher-order function. (注意对比理解)
看最后一个例子,定义一个类型具有 * -> (* -> *) -> *
的 kind,下面是一种定义的方法,
data Frank a b = Frank {frankField :: b a} deriving (Show)
怎么推断出这个类型具有 * -> (* -> *) -> *
的 kind 呢?
- ADT 中的字段是要来塞值的,所以其 Kind 必须是
*
; - 假设
a
是*
,那b
就是接受一个类型参数的 Kind,即* -> *
; - 现在知道 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]
withJust 2
to produceJust [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)]
, becauseord
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,
- We start off with
sequenceA [[1,2],[3,4]]
- That evaluates to
(:) <$> [1,2] <*> sequenceA [[3,4]]
- Evaluating the inner
sequenceA
further, we get(:) <$> [1,2] <*> ((:) <$> [3,4] <*> sequenceA [])
- We've reached the edge condition, so this is now
(:) <$> [1,2] <*> ((:) <$> [3,4] <*> [[]])
- Now, we evaluate the
(:) <$> [3,4] <*> [[]]
part, which will use:
with every possible value in the left list (possible values are3
and4
) 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]]
- Now,
:
is used with every possible value from the left list (1
and2
) 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.
网友评论