一、入门
一、入门
学习Haskell的契机是想要学习 函数式编程
函数式编程语言有很多,为什么选择 Haskell?因为 Haskell 是纯函数编程语言中最热门的
代码编辑器选择 VSCode,需要配置插件
下面会介绍一些会遇到的名词,可能会不太对(2023年3月12日之前 自己理解的)
- Haskell:编程语言,官网 https://www.haskell.org/
- GHCup:必装,管理工具,可以管理 Haskell 所需要的所有工具。注意和 ghc 不是一样的东西
- ghc:必装,Haskell 编译器,可以使用其来编译 .hs 文件
- HLS:必装,Haskell Language Server,Haskell 语言服务器。不会直接 HLS,但 VSCode 需要在后台使用 HLS
- Cabal:Haskell 项目构建工具。简单说就是脚手架,用于生成项目结构
- Stack:Haskell 项目构建工具。也是脚手架,学习视频会使用 Cabal 或者 Stack,建议两个都转上。
- 以上在 GHCup 安装的时候都会提示安装,直接全部使用推荐版本,如果使用最新版本可能会有兼容性问题。
二、安装
2.1 安装GHCup
GHCup 官网:https://www.haskell.org/ghcup/
理论上一行代码就可以完成安装,就像官网所示。但是由于网络原因,需要手动安装。空降 https://www.haskell.org/ghcup/install/#manual-installation
下载
x86_64-mingw64-ghcup.exe,不要运行,放到一个不要那么深的目录下,例如C:\ghcup\bin
2.2 安装MSYS2
- 安装这工具因为 GHCup 需要使用
- 下载链接: https://repo.msys2.org/distrib/msys2-x86_64-latest.exe
- 运行安装,要记住安装路径,例如
C:\msys64
2.3 配置环境变量
- 在 Path 添加 GHCup 的位置,例如
C:\ghcup\bin - 在用户变量新建一个变量,变量名填写
GHCUP_MSYS2,变量值填写C:\msys64 - 再新建一个变量,变量名为
GHCUP_CURL_OPTS,变量值填写-k。这个变量值的作用是跳过 CURL 的安全检查,CURL 在下载的时候会检查证书,会因为网络原因失败 - 然后在 GHCup 的配置文件配置镜像,配置文件位置 :安装路径下的
config.yaml - TODO:第3点和第4点是否只需要配置一个?需要后续验证
url-source:
OwnSource: https://mirrors.ustc.edu.cn/ghcup/ghcup-metadata/ghcup-0.0.7.yaml
然后打开 powershell ,运行下面代码
ghcup install ghcghcup install cabalghcup install stackghcup install hls
安装好后,可以运行 ghcup list 查看已安装的版本
如果想要切换工具链的版本,ghcup set ghc/stack/cabal/hls V.V.V ,其中 V.V.V 代表工具的bj版本
2.4 配置stack
- 首先找到 stack 的安装位置,我的安装位置是
C:\Users\xxxxx\AppData\Roaming\stack,其中 xxxxx 是电脑的用户名。如果不知道的话,可以输入命令stack path,然后在输出信息的第 4 行,找到变量名为stack-root - 然后打开
config.yaml,这个是 stack 的主配置文件,并复制下面的代码
# 配置使用系统ghc
system-ghc: true
# 配置镜像
setup-info-locations: ["https://mirrors.tuna.tsinghua.edu.cn/stackage/stack-setup.yaml"]
urls:
latest-snapshot: https://mirrors.tuna.tsinghua.edu.cn/stackage/snapshots.json
snapshot-location-base: https://mirrors.tuna.tsinghua.edu.cn/stackage/stackage-snapshots/
- 在这里也可以配置创建项目的时候,自动添加作者名字、版权声明等信息
templates:
params:
# author-name:
# author-email:
# copyright:
# github-username:
- 安装 msys2,输入
stack setup。我暂时没找到使用在 2.2 节安装的 msys2,所以就只能再安装一个。
2.5 创建项目
- 输入命令
stack new project_name - 然后启动命令
stack build,如果没有报错则没有问题 - 配置插件,将下面代码添加到 json 文件里
// 设置用GHCup来管理HLS
"haskell.manageHLS": "GHCup",
// 设置GHCup的安装位置
"haskell.serverEnvironment": {
"PATH": "C:/ghcup/bin"
},
// 设置使用全局的工具链
"haskell.toolchain": {
"ghc": null,
"cabal": null,
"stack": null
},
- 最后 打开/重启打开 vscode,最后的效果如下

三、基本语法
学习基本语法,先不使用 vscode,先使用最简便的 ghci ,直接在命令行输入 ghci ,就可以进入。
历史:函数式编程和数学关系非常紧密,不像平常的面向对象和面向过程,很少使用到数学知识。如果说要用到数学知识的方向,估计就是算法之类的了。(陋见:平常写业务代码也使用不到红黑树之类的吧)
所以先从数学方面来学习基本语法,而不是像平常语言一样学习语句。
本章书籍链接:
- https://flaneur2020.github.io/lyah/ready-begin.htm
- http://cnhaskell.com/
3.1 简单运算
- 加减乘除没问题
ghci> 2 + 15
17
ghci> 1823 - 1354
469
ghci> 7 / 4
1.75
ghci> 3 * 39
117
- 使用括号改变计算顺序
ghci> 10 + 8 * 20
170
ghci> (10 + 8) * 20
360
- 负数也要加上括号,就像 小学/初中 课本一样。但也和现在默认的一样,只要负号前面没有运算符,括号可以省略

# 负号在前面可以省略括号
ghci> -3 * 5
-15
# 负号在前面有运算符需要加上括号
ghci> 5 * -3
<interactive>:12:1: error:
Precedence parsing error
cannot mix ‘*’ [infixl 7] and prefix `-' [infixl 6] in the same infix expression
ghci> 5 * (-3)
-15
- 逻辑运算,与、或、非,
True和False记住 首字母要大写
ghci> True && False
False
ghci> True && True
True
ghci> False || True
True
ghci> not False
True
- 相等性判断,不等于使用
/=,而不是!=了,/=更像不等号
ghci> 6 == 6
True
ghci> 6 == 0
False
ghci> 6 /= 6
False
ghci> 6 /= 0
True
ghci> "hello" == "hello"
True
- 进行运算的时候,需要左右两边都是一样的类型。不然就像现实生活中说,你的年龄比我昨天睡得还要晚。(胡言乱语.jpg)
ghci> 5 + "1"
<interactive>:19:3: error:
? No instance for (Num String) arising from a use of ‘+’
? In the expression: 5 + "1"
In an equation for ‘it’: it = 5 + "1"
ghci> 5 == "1"
<interactive>:20:1: error:
? No instance for (Num String) arising from the literal ‘5’
? In the first argument of ‘(==)’, namely ‘5’
In the expression: 5 == "1"
In an equation for ‘it’: it = 5 == "1"
- 我们可以用命令
:info来查看某个操作符的优先级,或者某个类型的详细信息
- 第 1 ~ 4 行可以看到 String 其实是 Char 字符类型的数组,这点和 C 语句很像;以及这个类型在 GHC的Base 里定义的(是文件还是类?)
- 第 6 ~ 12 行可以看到
+函数需要传入两个 Num 值;在 GHC 的 Num 里定义的;infixl 6 +,表示(+)的 左结合优先级 是 6;而infixr 8,表示(^)的 右结合优先级 是 8
ghci> :info String
type String :: *
type String = [Char]
-- Defined in ‘GHC.Base’
ghci> :info +
type Num :: * -> Constraint
class Num a where
(+) :: a -> a -> a
...
-- Defined in ‘GHC.Num’
infixl 6 +
ghci> :info ^
(^) :: (Num a, Integral b) => a -> b -> a -- Defined in ‘GHC.Real’
infixr 8 ^
- 乘方操作符的两种形式,如果指数是整数,使用
^符号;如果是小数,使用**符号。
ghci> 4 ** 2.5
32.0
3.2 基本类型
1)列表
一个列表由方括号以及被逗号分隔的元素组成。列表里所有的元素必须是相同类型。(万恶的 Javascript)
ghci> [1, 2, "3"]
<interactive>:15:2: error:
? No instance for (Num String) arising from the literal ‘1’
? In the expression: 1
In the expression: [1, 2, "3"]
In an equation for ‘it’: it = [1, 2, "3"]
ghci> [1, 2, 3]
[1,2,3]
如果用 列举符号 .. 来表示一系列元素,Haskell则会根据规律自动填充内容,但只使用于等差数列。在计算一些数学公式的时候会很有用。
ghci> [1, 2..10]
[1,2,3,4,5,6,7,8,9,10]
ghci> [1, 3..10]
[1,3,5,7,9]
ghci> [10, 9..1]
[10,9,8,7,6,5,4,3,2,1]
ghci> [2, 4, 8, 16..64] # 原意是 [2, 4, 8, 16, 32, 64]
<interactive>:27:13: error: parse error on input ‘..’
如果类型是浮点数的话,也会像常见的编程语言一样,会很怪异。(详情可以搜索浮点数在计算机里的存储方式)
ghci> [1, 1.2..2]
[1.0,1.2,1.4,1.5999999999999999,1.7999999999999998,1.9999999999999998]
列表的操作大概有两种:一种是 ++ ,用于连接两个列表,很简单,毕竟在平常使用中要连接两个列表只需要擦掉中间连接的括号;一种是 元素 : 列表 ,用于在列表的头部添加元素。
ghci> [1, 2] ++ [3, 4]
[1,2,3,4]
ghci> "Hello " ++ "World"
"Hello World"
ghci> 1 : [2, 3, 4]
[1,2,3,4]
2)开启类型显示
- 使用
:set +t开启类型显示;it代表最近一次求的值所存到的变量
ghci> ['h', 'e', 'l']
"hel"
it :: [Char]
ghci> "hel"
"hel"
it :: String
- 使用
:unset +t关闭类型显示
ghci> "World"
"World"
- 在关闭类型显示的时候,可以使用
:type xxxxx来显示某个值的类型
ghci> :type "!"
"!" :: String
3)数字
数字在 Haskell 里比较复杂。简单来说,非小数的数字有两种,一个是 Integer ,无界整数;另一个是 Int ,有界整数,他们的父类(构造函数)是 Integral 。
无界整数,换句话来说就是 bignums 类型,可以用来防止数字溢出。
注意
ghc 9.2.5 版本会报错,在 ghc 9.2.8 修复了这个 Bug
ghci> 2 ^ 1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
有小数的数字可以查看官方文档:https://www.haskell.org/tutorial/numbers.html
4)元组
- 元组长度固定,里面的每个元素类型可以不同。用于存放一个东西的不同信息,例如一本书的书名、价格、持有数量等
- 列表长度不固定,里面的每个元素类型必须相同。用于存放不同东西的相同信息,例如一个图书馆的所有书的价格
从下面的代码可知元组的类型就是 信息类型的集合
ghci> ("Kiniro Mosaic", "YUI", 2010)
("Kiniro Mosaic","YUI",2010)
ghci> :type ("Kiniro Mosaic", "YUI", 2010)
("Kiniro Mosaic", "YUI", 2010) :: Num c => (String, String, c)
5)变量
变量在 Haskell 可以成为常量,因为当与一个表达式绑定后,那么这个变量的值就不会改变。
实际原因是 Haskell 的初心,一个表达式无论外界发生了什么变化,其返回的值都不应该变化
如果在编辑器上像下面这样写,就会报错,但在 ghci 里这样写可以正常替换
x = 2
x = 1
-- Multiple declarations of ‘x’
-- Declared at: E:\haskell\test.hs:1:1
-- E:\haskell\test.hs:3:1 typecheck
3.3 类型推导
Haskell 最大的一个特点就是类型推导,会将各种变量在编译的时候推导出来,然后再判断是否正确。既能在编译过程中找出错误,而且不用写那么多类型签名。
3.3.1 变量推导
可以在 ghci 命令行中进行变量推导
- 如果直接输入值,即使不指定类型,也合法
- 如果指定正确的类型也合法
- 如果指定错误的类型会报错
ghci> 'a' -- 自动推导
'a'
ghci> 'a' :: Char -- 正确的类型
'a'
ghci> 'a' :: Int -- 错误的类型
<interactive>:3:1: error:
? Couldn't match expected type ‘Int’ with actual type ‘Char’
? In the expression: 'a' :: Int
In an equation for ‘it’: it = 'a' :: Int
3.3.2 函数类型
使用 :type 函数名 ,可以看到该函数的类型推导,从类型推导可以知道如何使用函数。
例如 :type odd
- 大概的格式是
函数名 :: 变量声明 => 传参, ... -> 返回值 - 则根据下面的代码可知:
- 函数名是
odd,奇数的意思 - 声明了一个类型为
Integral的变量a - 然后参数需要传入类型为
Integral的值,然后返回一个Bool类型的值
- 函数名是
ghci> :type odd
odd :: Integral a => a -> Bool
3.4 函数使用
- 其实我们一直在使用函数,分为前缀函数 和 中缀函数。中缀函数就是 3.1 所提及的基本运算,运算符/函数 在中间;前端函数就是最常见函数的调用方法:
函数名 参数1 参数2 ...
ghci> succ 8 <# succ函数返回一个数的后继 #>
9
ghci> min 9 11 <# min函数返回两个数的最小值 #>
9
ghci> max 99 101 <# max函数返回两个数的最大值 #>
101
- 函数调用拥有最高的优先级,如下两句是等效的
ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1 <# 10 + 5 + 1 #>
16
- 如果某函数有两个参数,也可以用 [`] 符号将它括起,以中缀函数的形式调用它。例如 例如除法的 div 函数,中缀函数更符合数学的思维。注:[`] 符号一定需要,否则会报错
ghci> div 90 9
10
ghci> 90 `div` 9
10
ghci> 90 div 9
<interactive>:4:1: error:
- 对于同一个运算优先级的要注意 运算的先后顺序,尤其是函数调用。例如下面代码。
如果没用括号,compare 函数会认为我们传了四个参数
Prelude> compare (sqrt 3) (sqrt 6)
LT
Prelude> compare sqrt 3 sqrt 6
<interactive>:17:1:
The function `compare' is applied to four arguments,
but its type `a0 -> a0 -> Ordering' has only two
In the expression: compare sqrt 3 sqrt 6
In an equation for `it': it = compare sqrt 3 sqrt 6
3.3 定义函数
- 定义函数:
函数名 参数1 参数2 ... = 返回值,例如doubleUs x y = x*2 + y*2 - 在 ghci 里加载写好的 haskell 文件:
:load xxxx.hs,当然命令行和加载的文件要在同一个位置,否则要输入与命令行的相对位置 - 使用
:?可以查看全部的 ghci 命令 - 不使用
return关键字来返回函数值,因为一个函数就是一个表达式,不是多个语句,表达式算出来的值就是返回值 - 变量其实是常量:在 Haskell 程序里面, 当变量和表达式绑定之后, 程序会用变量替换成这个表达式,如果变了后面使用这个变量就会出错
- 因为函数就代表一个表达式,所以在函数里起一行写
x = 1是不行的,函数里如果要用变量有其他方式。
3.4 条件求值
如果函数有两个分支,可以使用 if 表达式,下面为实例代码
if表达式then一个分支返回的值
else另一个分支返回的值
englishOne :: (Eq a, Num a) => a -> String
englishOne x =
if x == 1
then "one"
else "None"
- 因为函数是用表达式来返回值的,所以
if语句的then和else后面也是接表达式 - 因为函数有自动推导,所以一定要有
else,而且then和else返回的值的类型一定要一致,否则就会报错
3.5 Haskell里的多态
Haskell 里有很多多态,例如下面代码,last 函数可以传入数字数组和字符串数组,且都能正确输出结果
Prelude> last [1, 2, 3, 4, 5]
5
Prelude> last "baz"
'z'
查看 last 的函数签名,可以看到下面说明。传入了个类型为 a 的数组,然后返回一个类型为 a 的值。
这个 a 就是类型变量,也就是说这个 a 代表类型,而且是可以变的
Prelude> :type last
last :: [a] -> a
a在函数传入[1, 2, 3, 4, 5],就会变成Int,则函数签名就变成了[Int] -> Inta在函数传入"baz",就会变成Char,则函数签名就变成了[Char] -> Char
3.6 多参数函数
可以传入多个参数的函数,称为多参数函数。但多参数函数不一定要传入多个函数,因为 Haskell 自带柯里化。
例如 take 函数,查看它的函数签名:
Prelude> :type take
take :: Int -> [a] -> [a]
通过函数签名可知,传入 Int 后再传入 [a] ,最后才会返回 [a],以常用编程语言的角度来想其实也没错。
而以 Haskell 的角度来看,其实是 Int -> ([a] -> [a]) ,就是传入 Int 后会 返回一个新函数,而这个新函数,以 [a] 为参数,并返回 [a]
当然也可以查看这个新函数的签名
ghci> :type take 2
take 2 :: [a] -> [a]
四、定义类型
4.1 定义新的数据类型
新的数据类型,就有点像 Java 里的类。不同的数据类型,或者说不同的类,里面的属性不一致。例如定义一个书类型,里面有 id 属性,书名属性和作者属性。
下面是 Java 定义的类
public class BookEntity {
int id;
String bookName;
String[] bookAuthors;
}
下面 Haskell 定义新的数据类型
-- file: ch03/BookStore.hs
data BookInfo = Book Int String [String]
deriving (Show)
BookInfo:叫做类型构造器,但他是数据类型,而不是创造类型的。个人感觉叫为 【值构造器工厂】会好点。Book:值/数据 构造器,和 Java 的构造器意思一样。因为使用构造器,可以创造出 值/数据 来,所以称为 值/数据 构造器。(其实很少人这么说吧)- 后面跟着的数据类型,是属性的类型,
Int是书 ID 的类型,而String书名的类型,而[String]则是作者们的类型
下面在 ghci 加载 BookStore 文件,并用 Book 来创造一个对象
ghci> myInfo = Book 9780135072455 "Algebra of Programming" ["Richard Bird", "Oege de Mo]
ghci> myInfo
Book 9780135072455 "Algebra of Programming" ["Richard Bird","Oege de Moor"]
但要注意一点的是,myInfo 的类型是 BookInfo ,而不是 Book ,和上面说的一样,这个是和 Java 有点不像了。
Book是值构造器,换句话来说是个 函数,而不是类型BookInfo是数据类型
下面使用 :type 和 :info 来验证一下
ghci> :type myInfo
myInfo :: BookInfo
ghci> test = "233"
ghci> :type test
test :: String
ghci> :info BookInfo
type BookInfo :: *
data BookInfo = Book Int String [String]
ghci> :info String
type String :: *
type String = [Char]
-- Defined in ‘GHC.Base’
4.2 定义别名
当我们定义数据类型的时候,值构造器后面接着只有类型,并不知道这些类型代表着什么。String 类型,不仅可能代表书名,也可能代表出版社等。
-- file: ch03/BookStore.hs
data BookInfo = Book Int String [String]
deriving (Show)
所以使用 type 关键词可以为这些类型取个别名
type CustomerID = Int
type ReviewBody = String
data BetterReview = BetterReview BookInfo CustomerID ReviewBody
4.3 代数数据类型
标题有点难懂,换句话来说就是 Java 的重载。例如下面代码
type CardHolder = String
type CardNumber = String
type Address = [String]
data BillingInfo = CreditCard CardNumber CardHolder Address
| CashOnDelivery
| Invoice CustomerID
deriving (Show)
这段代码代表了三种付款方式:
- 第一种是用信用卡支付,需要输入卡号,卡的持有者和住址;
- 第二种方式是现金支付,不用任何参数;
- 第三种是货到付款,只需要填写客户的ID。
对应的就是 Java 的构造函数的重载,三参、无参和一参构造函数
4.4 代数数据类型和元组区别
- 元组:当两个变量里的类型和数据相等时,这个两变量比较就是相等的
- 代数数据类型:当两个变量即使里面的类型和数据相等时,这两个变量比较也是不相等的
总的来说代数数据类型就是 元组 加上了类型。下面代码分别定义了两个变量,然后分别赋值成相同的内容,进行比较发现相同
ghci> a = (1, "2", "3")
ghci> b = (1, "2", "3")
ghci> a == b
True
下面代码用两种方式表示了二维向量,分别是极坐标坐标和笛卡尔坐标。很明显这两个类型虽然都是由两个 double 组成,但是两个类型和这两个 double 代表的意思完全不一样。
-- file: ch03/AlgebraicVector.hs
data Cartesian2D = Cartesian2D Double Double
deriving (Eq, Show)
-- Angle and distance (magnitude).
data Polar2D = Polar2D Double Double
deriving (Eq, Show)
4.5 和C的区别
1. 结构
当只有一个值构造器时,代数数据类型和元组很相似,因为它将很多的值打包成一个复合值,和 C 里的 struct 、Java 里的 Object 一样。
以下是一个 C 结构,它等同于我们前面定义的 BookInfo 类型:
struct book_info {
int id;
char *name;
char **authors;
};
以下是一个 Java 结构,它等同于我们前面定义的 BookInfo 类型:
class BookInfo {
int id;
String name;
String authors;
}
而 haskell 是如下定义的
--file: ch03/BookStore.hs
data BookInfo = Book Int String [String]
deriving (Show)
2. 枚举
C 的 enum 通常用于表示一系列符号值排列。以下是一个 enum 例子:
enum roygbiv {
red,
orange,
yellow,
green,
blue,
indigo,
violet,
};
以下是等价的 Haskell 代码:
-- file: ch03/Roygbiv.hs
data Roygbiv = Red
| Orange
| Yellow
| Green
| Blue
| Indigo
| Violet
deriving (Eq, Show)
然后在 ghci 里面测试:
ghci> Red
Red
ghci> :type Red
Red :: Roygbiv
ghci> Red == Yellow
False
ghci> Red == Red
True
3. 联合
如果一个数据类型有多个备选,那么可以将它看作是 C 里的 union。也就是说这个数据类型是可以变的,分配内存以占用最大的类型为准。
以下是一个 union 例子:里面有个 shape 的 type 用于存放当前 union 里是什么类型,因为我们使用的时候不知道联合体里是什么类型。如果搞错了备选的信息,那么对 union 的使用就会出错。
enum shape_type {
shape_circle,
shape_poly,
};
struct circle {
struct vector centre;
float radius;
};
struct poly {
size_t num_vertices;
struct vector *vertices;
};
struct shape
{
enum shape_type type;
union {
struct circle circle;
struct poly poly;
} shape;
};
一方面,Haskell 版本不仅简单,而且更为安全:因为我们把值构造器开放出来了,新建时因为有 模式匹配 不会出错,使用时,因为 值会记录是哪个值构造器创建的,也不会出错。
-- file: ch03/ShapeUnion.hs
type Vector = (Double, Double)
data Shape = Circle Vector Double
| Poly [Vector]
deriving (Show)
4.6 模式匹配
1. 组成和解构
对于 Haskell 来说,模式匹配 可以做到以下两点:
- 如果这个类型有一个以上的值构造器,我们可以知道这个值是由哪个构造器创建的。
- 如果一个值构造器包含不同的数据成分,那么可以获取到这些成分。
模式匹配允许我们查看值的内部,并将值绑定到变量上。以下是一个对 Bool 类型值进行模式匹配的例子,它的作用和 not 函数一样:
-- file: ch03/myNot.hs
myNot True = False
myNot False = True
初看上去,代码似乎同时定义了两个 myNot 函数,但实际情况并不是这样。Haskell 将 Java 里的重载函数定义为一系列等式。Java 的重载函数如果是相同类型则不能构成重载,而 Haskell 的重载更宽松点。
在 ghci 加载上面的代码,然后调用这个函数:
ghci> a = myNot False
ghci> a
True
当输入 False 时,然后在 myNot 里进行查找,首先匹配第一个 myNot ,因为输入的是 False ,不是 True ,所以继续查找。查找到第二个,发现匹配则运行等号后面的语句,发现只有个返回值,就将 True 返回出去
换句话来说就有点像 if + return 语句
以下是一个复杂一点的例子,这个函数使用递归来计算出列表所有元素之和:
-- file:: ch03/sumList.hs
sumList (x:xs) = x + sumList xs
sumList [] = 0
这里要说下 (x:xs) 是什么意思。(:) 是一个运算符,会将数组的第一个元素提取出来赋值给 x,然后将已被提取元素的数据赋值给 xs 。这个就有点像 Javascript 中的 解构
但这个运算符不仅可以解构,而且可以进行运算。这是个可以双向运算的运算符。
首先定义两个函数
headTest (x:xs) = x
shiftTest (x:xs) = xs
然后在 ghci 测试:
ghci> headTest [1, 2, 3, 4]
1
ghci> shiftTest [1, 2, 3, 4]
[2,3,4]
ghci> (1: [2, 3, 4])
[1,2,3,4]
ghci>
再回到刚才的代码:很明显是个递归,首先先将第一个元素提取出来,然后将剩余的数组再调用当前的函数,递归的结束条件是最后的数组是空的
-- file:: ch03/sumList.hs
sumList (x:xs) = x + sumList xs
sumList [] = 0
假设我们输入 [1, 2, 3, 4] ,然后代码会如下运行:
sumList (1: [2, 3, 4]) = 1 + sumList [2, 3, 4]sumList (1: [2, 3, 4]) = 1 + 2 + sumList [3, 4]sumList (1: [2, 3, 4]) = 1 + 2 + 3 + sumList [4]sumList (1: [2, 3, 4]) = 1 + 2 + 3 + 4 + sumList []sumList (1: [2, 3, 4]) = 1 + 2 + 3 + 4 + 0sumList (1: [2, 3, 4]) = 10
其实标准函数库里已经有 sum 函数,它和我们定以的 sumList 一样,都可以用于计算表元素的和。
对于 对象/值 也可以进行模式匹配:以下代码的作用是获取对象里的某个属性,类似于 Java 类中的 get 函数
data BookInfo = Book Int String [String]
deriving (Show)
bookID (Book id title authors) = id
bookTitle (Book id title authors) = title
bookAuthors (Book id title authors) = authors
然后在 ghci 测试:
ghci> let book = (Book 3 "Probability Theory" ["E.T.H. Jaynes"])
ghci> book
Book 3 "Probability Theory" ["E.T.H. Jaynes"]
ghci> bookID book
3
ghci> bookTitle book
"Probability Theory"
ghci> bookAuthors book
["E.T.H. Jaynes"]
2. 通配符模式匹配
如果在模式匹配中我们不在乎某个值的类型,那么可以用下划线字符 _ 作为符号来进行标识,它也叫做 通配符。它的用法如下。
nicerID (Book id _ _ ) = id
nicerTitle (Book _ title _ ) = title
nicerAuthors (Book _ _ authors) = authors
使用通配符有两个好处,第一个好处是能知道这个函数的用意,_ 就像挖出来的空,我们不需要留意这些内容;第二个好处是可以防止编译器发出警告,因为把所有的属性列出来但不使用。这些属性就会看作没有使用的变量,而一个好的编程规范要删除没有使用到的变量。
3. 通配符的另一个作用
通配符的另一个作用是可以保底函数的返回值,就像其他语言中只在 if 和 else if 中 return 了,这时编译器会有个 警告/错误 提示我们要在最后面加多一个 return ,这个最后面的 return 就是来保证这个函数一定有返回值。
下面举个不是很好的例子:我们写这个代码时,使用了 [] 来作为最终的返回值,其实这个没问题的,因为我们已经知道了函数传入的值是 列表 以及递归的最后参数是 空列表,所以 [] 就已经包括了全部情况。
sumList (x:xs) = x + sumList xs
sumList [] = 0
如果不想考虑这么多,将 [] 替换成 _ 也是没问题的
sumList (x:xs) = x + sumList xs
sumList _ = 0
4.7 记录语法
在通配符那节我们如下写了代码,虽然使用了通配符减少了一点工作量,但还是很麻烦。如果属性多的情况下,这些 "get" 方法就会占用很多空间,以及创建它们的时候也会做很多无用功。
nicerID (Book id _ _ ) = id
nicerTitle (Book _ title _ ) = title
nicerAuthors (Book _ _ authors) = authors
Java 中将这些 "get" 的方法一般都是使用注解来自动生成,而 Haskell 是使用 记录语法。
记录语法是在定义类型的时候,同时构造出属性的访问器,下面是记录语法的例子:customerID 是获取 ID 的访问器名称,: 后面是类型。
data Customer = Customer
{ customerID :: Int,
customerName :: String,
customerAddress :: [String]
}
deriving (Show)
定义了类型,我们有 两种方式 来新建对象。第一种就是正常的按定义的顺序来放置值,例如下面所示:
ghci> a = Customer 1 "1" ["1"]
ghci> a
Customer {customerID = 1, customerName = "1", customerAddress = ["1"]}
第二种是用键值对的形式来新建:下面代码特意将属性换了个位置,这种方式就算位置随便放也是可以成功新建的。
customer2 :: Customer
customer2 =
Customer
{ customerName = "hahg",
customerAddress = ["123"],
customerID = 123
}
虽然属性变换位置不会影响到新建对象,但打印输出的时候还是会有所不同
ghci> customer2
Customer {customerName = "hahg", customerID = 123, customerAddress = ["123"]}
标准库里的 System.Time 模块 就是一个使用记录语法的好例子,虽然这个模块已废弃。例如其中定义了这样一个类型:
data CalendarTime
= CalendarTime {
ctYear :: Int -- ^ Year (pre-Gregorian dates are inaccurate)
, ctMonth :: Month -- ^ Month of the year
, ctDay :: Int -- ^ Day of the month (1 to 31)
, ctHour :: Int -- ^ Hour of the day (0 to 23)
, ctMin :: Int -- ^ Minutes (0 to 59)
, ctSec :: Int -- ^ Seconds (0 to 61, allowing for up to
-- two leap seconds)
, ctPicosec :: Integer -- ^ Picoseconds
, ctWDay :: Day -- ^ Day of the week
, ctYDay :: Int -- ^ Day of the year
-- (0 to 364, or 365 in leap years)
, ctTZName :: String -- ^ Name of the time zone
, ctTZ :: Int -- ^ Variation from UTC in seconds
, ctIsDST :: Bool -- ^ 'True' if Daylight Savings Time would
-- be in effect, and 'False' otherwise
}
deriving (Eq,Ord,Read,Show)
4.8 多态类型
根据之前可知列表中的元素可以是任何类型,所以可以说列表里的类型是多态的。我们当然也可以给 自定义的类型 添加多态性,可以使用一个叫做 Maybe 的类型来添加多态性,它既可以表示有值也可能空缺。
下面是 Haskell 中如何定义 Maybe :
Just代表有值的意思- 这里的
a代表任何的值,而Just值构造器可以接受任何类型的值,然后返回Maybe a的类型 - 当传入
a后,返回的Maybe类型的值就叫Just a,虽然看起来像是在调用值构造器,但其实代表一个值
- 这里的
Nothing代表没有值的意思
data Maybe a = Just a
| Nothing
下面用 ghci 测试上面所提到的:
- 使用
Just传入1后,Just 1就变成了一个Maybe Num的
ghci> :type Just
Just :: a -> Maybe a
ghci> :type Just 1
Just 1 :: Num a => Maybe a
ghci> :type Just "111"
Just "111" :: Maybe String