Haskell
Table of Contents
1. Haskell 简介
Haskell is a standardized, general-purpose purely functional programming language, with non-strict semantics and strong static typing. It is named after logician Haskell Curry. The first version of Haskell ("Haskell 1.0") was defined in 1990.
参考:
Haskell Website: https://www.haskell.org/
Haskell 2010 Language Report: https://www.haskell.org/onlinereport/haskell2010/
Haskell 2010 Language Report (pdf): https://www.haskell.org/definition/haskell2010.pdf
Haskell Documentation: https://www.haskell.org/documentation
Programming in Haskell(较好的入门书籍,本文很多内容摘自于此书)
Real World Haskell: http://book.realworldhaskell.org/read/
Real World Haskell 中文版:http://cnhaskell.com/
Learn You a Haskell for Great Good!: http://learnyouahaskell.com/chapters
1.1. Haskell 编译器 GHC
GHC (Glasgow Haskell Compiler) 是开源的 Haskell 编译器。GHC 起源于英国的格拉斯哥大学(University of Glasgow)。
GHC 主要包含下面命令行工具:
命令行工具 | 描述 |
---|---|
ghc | An optimizing compiler that generates fast native code |
ghci | An interactive interpreter and debugger |
runghc (runhaskell) | A program for running Haskell programs as scripts, without needing to compile them first |
1.1.1. 初次使用 ghci
ghci
是个交互式的 Haskell 环境。下面是一些简单测试:
$ ghci GHCi, version 7.8.4: http://www.haskell.org/ghc/ :? for help Prelude> 1+2 3 Prelude> 4*5 20 Prelude> (2+3)*4 20 Prelude> 7 / 2 3.5 Prelude> 7 `div` 2 -- same as “div 7 2” 3 Prelude> 7 `mod` 2 -- same as “mod 7 2” 1 Prelude> sum [1, 2, 3, 4] 10 Prelude> sqrt 2 1.4142135623730951 Prelude> "Hello" ++ " Haskell" "Hello Haskell" Prelude> print "Hello Haskell" "Hello Haskell"
1.1.2. GHCi 提示符中常用命令
下面是 GHCi 提示符中常用命令,更多命令可输入 :?
或者参考 GHCi commands 。
Command | Meaning |
---|---|
:load file1 | load script file1 |
:reload | reload current script |
:set editor name | set editor to name |
:edit file1 | edit script file1 |
:edit | edit current script |
:type expr | show type of expr |
:info name | display information about the given name |
:? | show all commands |
:quit | :quit GHCi |
注:上面命令可以使用简写,如 :l file1
相当于 :load file1
,其它类似。
1.2. Hello world 程序
Haskell 程序的入口程序是 Main 模块的 main 函数。
下面是 Hello world 程序(假设文件名为 hello-world.hs):
main = putStrLn "Hello world"
执行方式一:使用 ghc
把它编译为二进制可执行程序后,再执行。
$ ghc hello-world.hs -o hello-world [1 of 1] Compiling Main ( hello-world.hs, hello-world.o ) Linking hello-world ... $ ./hello-world Hello world
执行方式二:使用 runghc
直接执行它。
$ runghc hello-world.hs Hello world
1.3. 程序文件结构
Haskell 程序可保存到文件(一般使用.hs 后缀)中。
假设文件 test.hs 的内容为:
double x = x + x -- 定义了名为double的函数
ghci
后接文件名,可以加载文件内容到 Haskell 交互式环境中(也可以启动交互式环境后使用 :load
命令加载文件)。如:
$ ghci test.hs GHCi, version 7.8.4: http://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Main ( test.hs, interpreted ) Ok, modules loaded: Main. *Main> double 3 6
如果文件 test.hs 修改了,可以通过在 GHCi 提示符中输入 :reload
命令来重新加载文件。
1.3.1. Program Structure
The abstract syntactic and semantic structure of Haskell:
(1) At the topmost level a Haskell program is a set of modules. Modules provide a way to control namespaces and to re-use software in large programs.
(2) The top level of a module consists of a collection of declarations, of which there are several kinds. Declarations define things such as ordinary values, datatypes, type classes, and fixity information.
(3) At the next lower level are expressions. An expression denotes a value and has a static type; expressions are at the heart of Haskell programming “in the small.”
(4) At the bottom level is Haskell’s lexical structure. The lexical structure captures the concrete representation of Haskell programs in text files.
参考:https://www.haskell.org/onlinereport/haskell2010/haskellch1.html
1.3.2. 注释
--
开始的内容是单行注释。不过,如果 --
是一个合法词法 token 的一部分,则不是注释。比如 --foo
是注释,而 -->
或者 |--
都不是注释。
{-
和 -}
之间的内容是多行注释。
1.3.3. 标识符
An identifier consists of a letter followed by zero or more letters, digits, underscores, and single quotes.
Identifiers are lexically distinguished into two namespaces: those that begin with a lowercase letter (variable identifiers) and those that begin with an upper-case letter (constructor identifiers).
标记符首字母为小写时称为“variable identifiers”,首字母为大写时称为“constructor identifiers”。如:
fun1 x = 2 * x -- 定义函数,函数名必须是小写字母开头 type Pos = (Int, Int) -- 定义类型,类型名必须是大写字母开头
参考:https://www.haskell.org/onlinereport/haskell2010/haskellch2.html#x7-180002.4
1.3.3.1. Reserved Identifiers
reservedid → case | class | data | default | deriving | do | else | foreign | if | import | in | infix | infixl | infixr | instance | let | module | newtype | of | then | type | where | _
1.3.4. 缩进
在 Haskell 中,缩进是重要的,同一层次的东西必须要有相同的缩进。图 1 是一些例子,演示了错误和正确的缩进。
Figure 1: Haskell 中一些错误和正确的缩进
可以用大括号把把一组语句括起来,语句之间用分号 ;
隔开,这时缩进无关紧要(不推荐这么做,因为这样代码很难看)。
比如:
a = b + c where b = 1 c = 2 d = a * 2
又可以写为:
a = b + c where {b = 1; c = 2} d = a * 2
还可以写为一行:
a = b + c where {b = 1; c = 2}; d = a * 2
1.4. 函数调用风格
Haskell 是函数式语言。函数在 Haskell 中占据非常重要的位置,不过,函数调用的风格和数学中函数的调用是有差异的。见表 3 所示。
Mathematics | Haskell |
---|---|
f x | |
f x y | |
f (g x) | |
f x (g y) | |
f x * g y |
2. Types and classes
2.1. Types 基本概念
A type is a collection of related values.
Haskell 是“静态类型”的语言,表达式的类型在编译阶段就已经确定了。 这使得代码更加安全,比如你尝试对字符串进行除法运算时,编译器会拒绝这样的代码。
Haskell 具有强大的“类型推导”系统,你往往可以省略类型的声明,编译器自己会推导出具体类型。
2.1.1. 查看类型(:type)
在 GHCi 中,使用 :type expr
可以查看表达式的类型。如:
Prelude> :type True True :: Bool Prelude> :type False False :: Bool Prelude> :type 'a' 'a' :: Char Prelude> :type "abc" "abc" :: [Char] Prelude> :type 123 123 :: Num t => t
注:记号 v :: T
表示 v 具有类型 T。
注:在程序中,可以使用模块 Data.Typeable
中的 typeOf
来得到表达式类型。
2.1.1.1. Type Signature (::
)
符号 ::
(后文将大量出现)表示 Type Signature。如 v :: T
表示 v 具有类型 T。
由于 Haskell 有类型推导系统,有时我们不用明确指定 Type Signature。不过,指定 Type Signature 会使程序更加容易理解。
参考:https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-800004.4
2.2. Basic types(Bool, Char, etc.)
表 4 是 Haskell 中的基本类型。
类型 | 说明 |
---|---|
Bool | 包含逻辑值 False 和 Ture |
Char | 单个字符,使用单引号包围,如'a', '3' |
String | 字符序列,使用双引号包围,如"abcd" |
Int | 整数,范围为 |
Integer | 可表示任意范围的整数 |
Float | 单精度浮点数 |
Double | 双精度浮点数 |
2.3. List types(元素类型相同)
Haskell 中,List 是“相同类型”元素组成的序列。List 的格式为元素之间使用逗号分隔,所有元素用方括号包围。如 [False, False, True]
。
如果 List 的元素类型是 T,则使用 [T]
表示该 List 的类型。如:
Prelude> :type [False, False, True] [False, False, True] :: [Bool] Prelude> :type ['a', 'b', 'c'] ['a', 'b', 'c'] :: [Char] Prelude> :type ["one", "two", "three"] ["one", "two", "three"] :: [[Char]]
List 的长度可以是无限的。
2.4. Tuple types(元素类型可以不同)
Tuple 是长度有限的元素序列,它的元素类型可以不相同。Tuple 的格式为元素之间使用逗号分隔,所有元素用小括号包围。如 (False, 'a', True)
。
如果 Tuple 的元素类型分别为 T1, T2, ..., Tn,则使用 (T1, T2, ..., Tn)
表示该 Tuple 的类型。如:
Prelude> :type (False, 'a', True) (False, 'a', True) :: (Bool, Char, Bool) Prelude> :type (False, "Hello") (False, "Hello") :: (Bool, [Char])
注:Tuple 的长度必须是有限的,这样才可能推导出 Tuple 的具体类型。
2.5. Function types
函数是从参数到返回值的映射。
如果函数的参数类型为 T1,返回值类型为 T2,则使用 T1 -> T2
表示该函数的类型。如:
Prelude> :type not not :: Bool -> Bool -- 函数not的参数类型是Bool,返回值类型也是Bool
下面是接受 Tuple 为参数的函数的例子。
add :: (Int, Int) -> Int -- 声明函数类型 add (x,y) = x+y -- 定义函数add,它的参数是一个Tuple,返回值是Int
在交互环境中声明函数类型比较麻烦,你可以先保存到文件中,再使用 :load file1.hs
命令加载到交互环境中,再用 :type add
来查看其函数 add 的类型。
函数 add 的类型为 (Int,Int) -> Int
,表明它接收一个类型为 (Int, Int)
的参数(Tuple 类型),返回值类型为 Int。
上面介绍的函数类型,其参数个数都为 1,下节将介绍参数个数大于 1 的函数的类型。
2.6. Curried functions
把接受多个参数的函数转换为接受单个参数的函数的过程,称为柯里化。Haskell 支持自动柯里化。比如,你定义了一个函数 add'
,它接受 2 个参数。如:
add' x y = x + y -- 这个函数(接受两个参数)和前面介绍的函数add(接受一个类型为Tuple的参数)不同
我们可以这样调用它: add' 1 2
(函数 add'
接受两个参数)。其实,我们还可以这样调用它: (add' 1) 2
,这表明 (add' 1)
返回另外一个函数,这个函数接受一个参数。这就是 Haskell 的自动柯里化。
如何表示函数 add'
的类型呢?可以这样 Int -> Int -> Int
,它等同于 Int -> (Int -> Int)
。
下面是一个接受 3 个参数的函数的例子。
mult :: Int -> Int -> Int -> Int -- same as Int -> (Int -> (Int -> Int)) mult x y z = x*y*z
2.7. Polymorphic types
库函数 head
可以得到任何类型 List 的首个元素。如:
Prelude> head [1, 3, 5] 1 Prelude> head [True, False] True
head
可接受任意类型的 List,那如何表示它的类型呢?常用 [a] -> a
来表示 head
的类型,其中小写字母 a
(也可使用其它小写字母,如 b
, c
等等)表示“多种类型”,函数 head
称为 Polymorphic function,其类型 [a] -> a
称为 Polymorphic type.
又如, take
也是一个 Polymorphic function,它的类型 Int -> [a] -> [a]
是 Polymorphic type.
Prelude> take 4 [1, 3, 5, 7, 9] [1,3,5,7] Prelude> :type take take :: Int -> [a] -> [a]
2.8. Overloaded types (=>
)
我们知道,函数 abs
可以对数字类型求绝对值,其中数字类型可以是整数,还可以是单精度浮点数,双精度浮点数等等。
Prelude> abs (-1) 1 Prelude> abs (-2.5) 2.5
怎么表示函数 abs
的类型呢?写为 Polymorphic 类型 a -> a
是不够准确的,因为 abs
接受的类型仅仅是数字类型。
函数 abs
的类型可记为 Num a => a -> a
(可以通过 :type abs
来验证)。 符号 =>
前面的内容 Num a
被称为 class constraint,它的含义是“type a is an instance of the class Num”。 关于 class Num
,下节将介绍。
函数 abs
称为 Overloaded function,其类型 Num a => a -> a
称为 Overloaded type.
下面是其它一些 Overloaded function:
Prelude> :type abs abs :: Num a => a -> a Prelude> :type negate negate :: Num a => a -> a Prelude> :type (*) (*) :: Num a => a -> a -> a Prelude> :type (+) (+) :: Num a => a -> a -> a
2.9. Basic classes
上一节介绍了 Class constraint,这里将介绍 Class 的概念。
Recall that a type is a collection of related values. Building upon this notion, a class is a collection of types that support certain overloaded operations called methods.
Class name | Description | Contains methods | Instances |
---|---|---|---|
Eq |
equality types | (==) :: a -> a -> Bool | Bool, Char, String, Int, Integer, Float, Double, and List, Tuple |
(/=) :: a -> a -> Bool | |||
Ord |
ordered types | (<) :: a -> a -> Bool | Bool, Char, String, Int, Integer, Float, Double, adn List, Tuple |
(<=) :: a -> a -> Bool | |||
(>) :: a -> a -> Bool | |||
(>=) :: a -> a -> Bool | |||
min :: a -> a -> a | |||
max :: a -> a -> a | |||
Show |
showable types | show :: a -> String | Bool, Char, String, Int, Integer, Float, Double, and List, Tuple |
Read |
readable types | read :: String -> a | Bool, Char, String, Int, Integer, Float, Double, and List, Tuple |
Num |
numeric types | (+) :: a -> a -> a | Int, Integer, Float, Double |
(-) :: a -> a -> a | |||
(*) :: a -> a -> a | |||
negate :: a -> a | |||
abs :: a -> a | |||
signum :: a -> a | |||
Integral |
integral types | (+) :: a -> a -> a | Int, Integer |
(-) :: a -> a -> a | |||
(*) :: a -> a -> a | |||
negate :: a -> a | |||
abs :: a -> a | |||
signum :: a -> a | |||
div :: a -> a -> a | |||
mod :: a -> a -> a | |||
Fractional |
fractional types | (+) :: a -> a -> a | Float, Double |
(-) :: a -> a -> a | |||
(*) :: a -> a -> a | |||
negate :: a -> a | |||
abs :: a -> a | |||
signum :: a -> a | |||
(/) :: a -> a -> a | |||
recip :: a -> a |
测试 method 类型时,会显示对应的 class 名字。如:
Prelude> :type (==) (==) :: Eq a => a -> a -> Bool Prelude> :type (<) (<) :: Ord a => a -> a -> Bool Prelude> :type show show :: Show a => a -> String Prelude> :type read read :: Read a => String -> a Prelude> :type negate negate :: Num a => a -> a Prelude> :type div div :: Integral a => a -> a -> a Prelude> :type (/) (/) :: Fractional a => a -> a -> a
下面是使用 method 的一些测试:
Prelude> [1, 2] == [1, 2] -- 判断是否相等 True Prelude> 1 /= 2 -- 判断是否不相等,Haskell中“不等于”用 /= 表示 True Prelude> show False "False" Prelude> show 123 "123" Prelude> read "False" :: Bool False Prelude> recip 3 -- 取倒数 0.3333333333333333
3. Defining functions
Haskell 中有很多方式可以定义函数,后文将介绍各种定义函数的方式。
3.1. New from old
定义函数最直接的办法是借助于已经存在的函数。
例如,定义测试偶数的函数:
even :: Integral a => a -> Bool even n = n `mod` 2 == 0
3.2. Conditional expressions (if ... then .. else ...)
通过 if ... then .. else ...
是定义函数的常用方式。
例如,内置函数 abs
可以像下面这样定义:
abs :: Int -> Int abs n = if n >= 0 then n else -n
if ... then .. else ...
可以嵌套使用。例如,内置函数 signum
可以像下面这样定义:
signum :: Int -> Int signum n = if n < 0 then -1 else if n == 0 then 0 else 1
注:和其它语言不同, if ... then .. else ...
中的 else
部分是不可以省略了,这样避免了“dangling else”问题。
3.3. Guarded equations (|
)
Haskell 中函数还可以通过“一系列逻辑表达式”来定义(这些逻辑表达式被称为 Guards)。如果第一个逻辑表达式成立,则采用第一个逻辑表达式后面关联的定义;如果第二个逻辑表达式成立,则采用第二个逻辑表达式后面关联的定义;依此类推。
例如,内置函数 abs
可以像下面这样定义:
abs n | n ≥ 0 = n | otherwise = −n -- 也可写为 True = -n ,不过 otherwise 更容易阅读
其中,符号 |
可理解为“such that”, otherwise
在内置库中被定义为 True
。
内置函数 signum
可以像下面这样定义:
signum n | n < 0 = −1 | n == 0 = 0 | otherwise = 1
3.4. Pattern matching
定义函数时,我们可以拆分函数主体为几个 pattern,然后分别对 pattern 进行定义。
比如,使用 Pattern matching,函数 not
可以这样定义:
not :: Bool -> Bool not False = True not True = False
又如,使用 Pattern matching,我们可以这样定义函数 intToWeekdayName
,它把数字转换为星期:
intToWeekdayName :: (Integral a) => a -> String intToWeekdayName 1 = "Monday" intToWeekdayName 2 = "Tuesday" intToWeekdayName 3 = "Wednesday" intToWeekdayName 4 = "Thursday" intToWeekdayName 5 = "Friday" intToWeekdayName 6 = "Friday" intToWeekdayName 7 = "Sunday"
又如,使用 Pattern matching,函数 factorial
可以这样定义:
factorial :: (Integral a) => a -> a factorial 0 = 1 factorial n = n * factorial (n - 1)
下面的例子演示了使用 Pathern matching 来定义操作符 &&
:
(&&) :: Bool -> Bool -> Bool True && True = True True && False = False False && True = False False && False = False
不过,上面 &&
的定义可以简化为:
(&&) :: Bool -> Bool -> Bool True && True = True _ && _ = False
其中,下划线 _
称为“wildcard pattern”,它能匹配任意值。 &&
还可以定义为下面形式:
(&&) :: Bool -> Bool -> Bool True && b = b -- if the first argument is True, then the result is the value of the second argument False && _ = False -- if the first argument is False, then the result is False
3.4.1. Tuple patterns
下面是 Tuple pattern 的几个例子:
first :: (a, b, c) -> a first (x, _, _) = x second :: (a, b, c) -> b second (_, y, _) = y third :: (a, b, c) -> c third (_, _, z) = z
3.4.2. List patterns (cons operator :
)
下面是 List patterns 的例子,当函数 startWithA
的参数是长度为 3 的字符串,且首字符为'a'时返回 True
,其它情况返回 False
。
startWithA :: [Char] -> Bool startWithA ['a', _, _] = True startWithA _ = False
怎么增强函数 startWithA
,使它接受任意长度字符串为参数,且仅当其以'a'开头时返回 True
呢?
首先,我们介绍一下 cons operator :
的概念。
Haskell 中,List 是由 cons 操作符 :
构造出来的。 :
的左操作符为一个元素,右操作符为 List。比如: [1]
由数字 1 和空列表经 :
操作后构成,即有 [1] == 1:[]
,类似地,有 [1, 2, 3] == 1 : [2, 3] == 1 : (2 : [3]) == 1 : (2 : (3 : []))
。
了解了 cons 操作符 :
的概念后,容易增强函数 startWithA
以满足前面的要求了。
startWithA :: [Char] -> Bool startWithA ('a':_) = True startWithA _ = False
3.5. Case expressions (case ... of ...)
Case 表达式语法如下:
case expression of pattern -> result pattern -> result pattern -> result ...
用 Case 表达式可以定义函数。如:
head' :: [a] -> a head' xs = case xs of [] -> error "No head for empty lists!" (x:_) -> x
上面的定义也可用“Pattern matching”来实现:
head' :: [a] -> a head' [] = error "No head for empty lists!" head' (x:_) = x
可以认为“Pattern matching”是“Case expressions”的语法糖。
3.6. Lambda expressions (\
)
Lambda expressions 就是匿名函数。
比如,接受一个数字 x
为参数,返回 x + x
的匿名函数可表示为: \x -> x + x
。测试如下:
Prelude> (\x -> x + x) 3 6
我们知道,函数 add
可定义为:
add :: Int -> Int -> Int add x y = x + y
如果使用 Lambda 表达式,则又可定义为:
add :: Int -> (Int -> Int) add = \x -> (\y -> x + y)
仅被引用一次的函数很多时间没必要给它取名字,这时往往改写为匿名函数。比如:
odds :: Int -> [Int] odds n = map f [0..n-1] -- map 是内置库函数,它的第一个参数是函数,第二个参数是列表。函数 f 的定义在下一行 where f x = x*2 + 1 -- where 表示后面的函数 f 是局部定义的
上面函数 odds
中定义的局部函数 f
仅被引用了一次,可以直接把它改写为 Lambda expressions(匿名函数)。
odds :: Int -> [Int] odds n = map (\x -> x*2 + 1) [0..n-1]
3.7. Operator sections
如果一个函数(如 +
)在调用时,函数名写在它的两个参数之间,则把它称为“操作符”。
任何具有两个参数的函数,其调用形式都可以转换为“操作符”调用形式。转换方法是把函数名用一对反引用括起来(如 `funName`
)。 例如:
Prelude> div 5 2 -- 函数调用形式 2 Prelude> 5 `div` 2 -- 操作符调用形式 2
相反地,任何操作符也可以转换为函数的调用形式。转换方法是把操作符用一对小括号括起来。 例如:
Prelude> 3 + 4 -- 操作符调用形式 7 Prelude> (+) 3 4 -- 函数调用形式 7
在 Haskell 中, (+) 3 4
还可以写为 (3+) 4
或者 (+4) 3
的形式。它们的前部分,即 (3+)
和 (+4)
都被称为 section。
一般地,如果 #
是操作符,下面三个形式被称为 operator section:
(#) -- 等同于 \x -> (\y -> x # y) (x #) -- 这称为left section,等同于 \y -> x # y (# y) -- 这称为right section,等同于 \x -> x # y
下面是 section 的一些例子:
section | equivalent to |
---|---|
(+) |
\x -> (\y -> x+y) |
(1+) |
\x -> 1 + x |
(+1) |
\x -> x + 1 |
(2^) |
\x -> 2 ^ x |
(^2) |
\x -> x ^ 2 |
('\t':) |
\x -> '\t' : x |
4. List comrehensions (<-
)
在数学的集合理论中,Comrehension 可以用来基于存在的集合构造新的集合。比如 Comrehension
类似地,在 Haskell 中,List comrehensions 可以用来基于存在的 List 构造新的 List,其语法和数学中的 Comrehension 很相似。比如:
Prelude> [ x^2 | x <- [1..5]] [1,4,9,16,25]
其中,符号 |
可以读做“such that”,符号 <-
可以读做“is drawn from”,而表达式 x <- [1..5]
被称为“generator”。
在一个 List comrehension 中可以使用多个“generator”。比如:
Prelude> [(x,y) | x <- [1,2,3], y <- [4,5]] -- two generators [(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)] -- 后面的(第二个)generator在结果中变化更快
注意,如果存在多个“generator”,则“generator”的顺序不同会得到不同的新列表。比如:
Prelude> [(x,y) | y <- [4,5], x <- [1,2,3]] -- 顺序和前面不同,得到的列表也不同 [(1,4),(2,4),(3,4),(1,5),(2,5),(3,5)] -- 后面的(第二个)generator在结果中变化更快
后面的 generator 可以引用了前面 generator 中的变量。比如:
Prelude> [(x,y) | x <- [1..3], y <- [x..3]] -- 后面的(第二个)generator引用了前面(第一个)generator中的变量(x) [(1,1),(1,2),(1,3),(2,2),(2,3),(3,3)]
下面是内置函数 concat
的一个实现:
concat :: [[a]] -> [a] concat xss = [x | xs <- xss, x <- xs] -- 第二个generator引用了第一个generator中的变量
concat
的使用实例:
Prelude> concat [ [1, 2, 3], [4, 5], [6, 7] ] [1,2,3,4,5,6,7]
wildcard pattern _
可以用在 generator 中。比如下面是内置函数 length
的一个实现:
length :: [a] -> Int length xs = sum [1 | _ <- xs]
4.1. Guards
可以使用逻辑表达式(被称为 Guards)对生成的结果进行过滤。比如:List comrehension [x | x <- [1..10], even x]
得到的结果为 [2,4,6,8,10]
。
4.2. String comprehensions
字符串其实就是字符列表。比如 "abc"
和 ['a','b','c']
是一回事。前面介绍的 List comprehensions 也适应于字符串。比如:
Prelude> [x | x <- "abc123XYZd", x >= 'a' && x <= 'z'] -- 用Guards过滤了非小写字母 "abcd"
下面是利用 String comprehensions 定义函数的两个例子:
lowers :: String -> Int lowers xs = length [x | x <- xs, x >= 'a' && x <= 'z'] -- 返回字符串中小写字母的数量 count :: Char -> String -> Int count x xs = length [x' | x' <- xs, x == x'] -- 返回字符串中包含指定字符的数量
5. Higher-order functions
由于 Haskell 支持自动柯里化。接收多个参数的函数,都可以返回另外一个函数。
假设有下面定义:
add :: Int -> Int -> Int add x y = x + y
如, (add 2)
返回另外一个函数。可以这样调用这个函数: (add 2) 3
,它的结果为 5
。
函数不仅可以返回另一个函数,还可以接收另一个函数为参数。
当一个函数接收另一个函数作为参数,或者返回另外一个函数作为结果时,这个函数可以称为“高阶函数”。
5.1. 一些列表处理高阶函数
内置函数 map
是一个常用的高阶函数。它的第一个参数是另一个函数,第二个参数是一个列表,返回的结果是对列表(第二个参数指定)中每个元素应用函数(第一个参数指定)后组成的新列表。
下面是 map
的一个实现:
map :: (a -> b) -> [a] -> [b] map f xs = [f x | x <- xs]
下面是 map
的一个使用实例:
Prelude> map (+1) [1,3,5,7] [2,4,6,8] Prelude> map even [1,2,3,4] [False,True,False,True] Prelude> map reverse ["abc","def","ghi"] ["cba","fed","ihg"]
下面是函数 map
的另外一个实现(利用递归调用):
map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs
表 7 是一些内置的列表处理高阶函数。
函数名 | 实例 |
---|---|
map | map even [1..4] 得到 [False, True, False, True] |
filter | filter even [1..10] 得到 [2, 4, 6, 8, 10] |
all | all even [2,4,6,8] 得到 True |
any | any even [2,3,4] 得到 True,any odd [2,3,4] 也得到 True |
takeWhile | takeWhile even [2,4,7,8,10] 得到 [2,4] |
dropWhile | dropWhile odd [1,3,5,6,7,8] 得到 [6,7,8] |
5.2. 函数 foldr
先观察几个函数的定义:
sum [] = 0 sum (x:xs) = x + sum xs or [] = False or (x:xs) = x || or xs and [] = True and (x:xs) = x && and xs
下面是 sum [1, 2, 3, 4]
的计算过程:
sum [1, 2, 3, 4] = sum 1 + (2 + (3 + (4 + 0))) = sum 1 + (2 + (3 + 4)) = sum 1 + (2 + 7) = sum 1 + 9 = 10
前面这些函数都有共同的处理模式,可以抽象为下面的形式:
f [] = 0 f (x:xs) = x # f xs
内置的高阶函数 foldr
(是“fold right”的缩写)封装了上面的模式。使用 foldr
可以更方便地定义 sum/or/and
等函数。如:
sum :: Num a => [a] -> a sum = foldr (+) 0 or :: [Bool] -> Bool or = foldr (||) False and :: [Bool] -> Bool and = foldr (&&) True
下面是函数 foldr
的一个实现:
foldr :: (a -> b -> b) -> b -> [a] -> b foldr f v [] = v foldr f v (x:xs) = f x (foldr f v xs)
如 foldr (+) 0 [1,2,3]
可以得到 1 + (2 + (3 + 0))
。下面总结了 foldr
的行为:
foldr (#) v [x0, x1, ..., xn] = x0 # (x1 # (... (xn # v) ...))
5.3. 函数 foldl
高阶函数 foldl
和 foldr
类似,不过它是左结合性的。下面是 foldl
的行为:
foldl (#) v [x0,x1,...,xn] = (... ((v # x0) # x1) ...) # xn
下面是函数 foldl
的一个实现:
foldl :: (a -> b -> a) -> a -> [b] -> a foldl f v [] = v foldl f v (x:xs) = foldl f (f v x) xs
sum/or/and
等函数除了用 foldr
定义外,还可以使用 foldl
定义。如:
sum :: Num a => [a] -> a sum = foldl (+) 0 or :: [Bool] -> Bool or = foldl (||) False and :: [Bool] -> Bool and = foldl (&&) True
采用上面定义时, sum [1, 2, 3, 4]
的计算过程如下:
sum [1, 2, 3, 4] = sum ((((0 + 1) + 2) + 3) + 4) = sum (((1 + 2) + 3) + 4) = sum ((3 + 3) + 4) = sum (6+ 4) = 10
5.4. 操作符 $
操作符 $
被称为“function application operator”,它的定义如下:
($) :: (a -> b) -> a -> b f $ x = f x
从定义上看,操作符 $
仅仅是应用一个函数。那它有什么用呢?
由于操作符 $
具有最低优先级,我们往往使用操作符 $
来去掉代码中的一些括号。 比如: sqrt (3 + 4 + 9)
可以写为 sqrt $ 3 + 4 + 9
,又如 sum (map sqrt [1..130])
可以写为 sum $ map sqrt [1..130]
。
此外,操作符 $
具有右结合性,也就是说 f $ g $ x
和 f $ (g $ x)
相等。所以下面三行代码是相同的:
sum (filter (> 10) (map (*2) [2..10])) sum $ filter (> 10) (map (*2) [2..10]) -- 利用 $ 去掉了一对括号 sum $ filter (> 10) $ map (*2) [2..10] -- 利用 $ 去掉了两对括号
5.5. The composition operator (.
)
内置高阶操作符 .
的左/右操作数都是函数,其返回的结果也是函数。下面是它的一个实现:
(.) :: (b -> c) -> (a -> b) -> (a -> c) f . g = \x -> f (g x)
把 f . g
读作“f composed with g”,它是接收一个参数的函数。首先把 g
应用它的参数,然后把 f
应用于上一步的结果。也就是说,它还可以定义为 (f . g) x = f (g x)
。 不过,之前的定义 f . g = \x -> f (g x)
更易理解,因为它明确地表示了 f . g
的结果是一个函数。
操作符 .
具备结合性,即有: f . (g . h) = (f . g) . h
利用 .
可以简化一些函数的定义。如:
odd n = not (even n) twice f x = f (f x) sumsqreven ns = sum (map (^2) (filter even ns))
利用 .
可以把上面的定义简化为:
odd = not . even twice f = f . f sumsqreven = sum . map (^2) . filter even
6. Declaring types and classes
6.1. Type declarations (type
)
定义 type 最简单的办法是基于已有的 type。比如:
type String = [Char] -- type名(String)必须以大写字母开头
又如:
type Pos = (Int,Int) type Trans = Pos -> Pos
6.2. Data declarations (data
)
A type is a collection of related values. 可以使用 data
为 type 指定相关的值。如:
data Bool = False | True -- type的值(False或True)也必须以大写字母开头
6.3. Newtype declarations (newtype
)
使用 newtype
可以定义全新的 type。如定义一个非负数:
newtype Nat = N Int
当然,我们可以这样定义非负数 Nat
:
type Nat = Int data Nat = N Int
不过,“采用 newtype
定义非负数 Nat
”比“采用 type
基于已有 type 定义非负数 Nat
”要更加安全。因为,如果采用 newtype
定义非负数 Nat
,则当程序期待非负数 Nat
时,你不能传递一个 Int
。
6.4. Class and instance declarations (class
, instance
)
节 2.9 介绍了一些基本的 class,这节介绍如何定义新的 class。
一个新的 class 可以使用 class
来定义。如内置 class Eq
可以这样定义:
class Eq a where (==), (/=) :: a -> a -> Bool -- 这里说明 class Eq 必须定义两个方法: == 和 /= x /= y = not (x == y) -- 这里是 default definition,这样Eq的实例只需要定义 == ,而无需定义 /= 了
使用 instance
可以定义 class 的实例。如 type Bool
可以这样定义:
instance Eq Bool where False == False = True True == True = True _ == _ = False
6.4.1. Extend to new class (=>
)
class 可以扩展为一个新 class。比如,内置 class Ord
可看作是 class Eq
的扩展。
内置 class Ord
可以这样定义:
class Eq a => Ord a where (<), (<=), (>), (>=) :: a -> a -> Bool min, max :: a -> a -> a min x y | x <= y = x -- default definition | otherwise = y max x y | x <= y = y -- default definition | otherwise = x
class Ord
的实例必须实现上面 6 个方法,不过 min
和 max
已经存在于 default definition 中,所以 class Ord
的实例只需定义其它 4 个方法。
下面演示了如何定义 type Bool
为 class Ord
实例:
instance Ord Bool where False < True = True _ < _ = False b <= c = (b < c) || (b == c) b > c = c < b b >= c = c <= b
6.4.2. Derived instances (deriving
)
在创建 type 时,可以在值构造子后面加上 deriving (Eq,Ord,Show,...)
,来给创建的 type 添加接口。如:
data Bool = False | True deriving (Eq, Ord, Show, Read)
7. Input and Output
Haskell 是一个纯粹函数式语言。函数唯一可以做的事是根据我们给定的参数来算出结果。函数是无副作用的,如果我们用同样的参数调用两次同一个函数,它会返回相同的结果。不过,事情没有这么完美。真实世界中,我们不得不接触“外部世界”,比如键盘、屏幕、文件等等,这些 I/O 相关操作。
下面是一个简单的 I/O 相关程序:
main = do putStrLn "Greetings! What is your name?" inpStr <- getLine putStrLn ("Welcome to Haskell, " ++ inpStr ++ "!")
函数 getLine
从标准输入读取一行。 运算符 <-
是用来从 I/O action 中抽出结果,并且保存到一个变量中(这里是 inpStr)。
上面程序测试运行如下:
$ runghc test.hs Greetings! What is your name? John Welcome to Haskell, John!
我们来看一下 getLine
和 putStrLn
的类型:
Prelude> :type getLine getLine :: IO String Prelude> :type putStrLn putStrLn :: String -> IO ()
表 8 是一些有用的 I/O 函数。
I/O 函数 | 描述 |
---|---|
putStr |
打印字符串到终端 |
putStrLn |
打印字符串到终端,外加一个换行符 |
putChar |
打印一个字符到终端 |
print |
相当于 putStrLn . show ,任何 Show 类型的实例都可以通过它打印出来 |
8. Monoids
模块 Data.Monoid 中定义了 Monoid
class,代码如下:
class Monoid m where mempty :: m mappend :: m -> m -> m mconcat :: [m] -> m mconcat = foldr mappend mempty -- default definition/implementation,所以这个方法用户可不实现
下面一一介绍 Monoid
实例所必须实现的 3 个方法。
(1) mempty
,它不接受任何参数,所以它并不是一个函数,而是一个 polymorphic 的常数, mempty
表示特定 monoid 的“identity value”。后面会介绍 List
是一种 Monoid,它的 mempty
为空列表 []
。
(2) mappend
,它是接受两个相同类型参数的函数,且会返回同样的类型。名字“mappend”取得不好,单从名字上看,应该是添加两个东西到一起变成同类型的另一个东西。不过,你不要被这个函数的名字误导,只要满足这个函数的 Type Signature(即 m -> m -> m
)都可以是 mappend
。后面会介绍 List
的 mappend
为操作符 ++
。
(3) mconcat
,它有默认的实现,一般情况下,你不用重新实现它。
8.1. The Monoid Laws
下面三个要求称为 Monoid Laws:
(1) mempty `mappend` x = x
(2) x `mappend` mempty = x
(3) (x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)
,即 mappend
遵守结合律。
注 1:Haskell 不会强制检查这些定律是否被遵守。我们在定义 Monoid 的实例时必须自己遵守它们。
注 2:Monoid 来源于抽象代数中的 幺半群(Monoid) 。
8.2. Lists Are Monoids
List
是 Monoid
的实例,其代码如下:
instance Monoid [a] where mempty = [] mappend = (++)
下面是一些基本测试:
Prelude> [1,2,3] `mappend` [4,5,6] [1,2,3,4,5,6] Prelude> mconcat [[1,2],[3,6],[9]] [1,2,3,6,9] Prelude> mempty :: [a] []
9. Monads
Monad
class 的定义如下:
class Monad m where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b x >> y = x >>= \_ -> y -- 这是 >> 的默认实现 fail :: String -> m a fail msg = error msg -- 这是 fail 的默认实现
Monad
class 的实例需要实现 4 个方法,不过,其中两个方法(即 >>
和 fail
)已有默认实现。
参考:
https://wiki.haskell.org/Monad
https://www.haskell.org/tutorial/monads.html
10. Appendix
10.1. 内置操作符的优先级和结合性
下表是 Haskell 中内置操作符的优先级和结合性。
+--------+----------------------+-----------------------+-------------------+ | Prec- | Left associative | Non-associative | Right associative | | edence | operators | operators | operators | +--------+----------------------+-----------------------+-------------------+ | 9 | !! | | . | +--------+----------------------+-----------------------+-------------------+ | 8 | | | ^, ^^, ** | +--------+----------------------+-----------------------+-------------------+ | 7 | *, /, `div`, | | | | | `mod`, `rem`, `quot` | | | +--------+----------------------+-----------------------+-------------------+ | 6 | +, - | | | +--------+----------------------+-----------------------+-------------------+ | 5 | | | :, ++ | +--------+----------------------+-----------------------+-------------------+ | 4 | | ==, /=, <, <=, >, >=, | | | | | `elem`, `notElem` | | +--------+----------------------+-----------------------+-------------------+ | 3 | | | && | +--------+----------------------+-----------------------+-------------------+ | 2 | | | || | +--------+----------------------+-----------------------+-------------------+ | 1 | >>, >>= | | | +--------+----------------------+-----------------------+-------------------+ | 0 | | | $, $!, `seq` | +--------+----------------------+-----------------------+-------------------+
参考:https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-820004.4.2