Haskell 入门:第 1 天学习指南
1. Haskell 简介
Haskell 对于许多函数式编程纯粹主义者来说,代表着纯粹和自由。它功能丰富且强大,但要掌握它也并非易事,就像你不能只浅尝辄止,而需要全身心投入到函数式编程的世界中。与 Scala、Erlang 和 Clojure 不同,Haskell 没有给命令式概念留下太多空间,是一门纯粹的函数式语言。
Haskell 是从底层开始构建的纯粹函数式语言,它融合了其他优秀函数式语言的思想,尤其强调惰性处理。它具有强大的静态类型系统,类型模型大多可以被推断,被广泛认为是函数式语言中最有效的类型系统之一,支持多态性和简洁的设计。此外,Haskell 还支持 Erlang 风格的模式匹配和守卫、Clojure 风格的惰性求值,以及来自 Clojure 和 Erlang 的列表推导式。
作为纯粹的函数式语言,Haskell 不产生副作用,而是函数可以返回一个副作用,后续再执行。在后续的学习中,我们会看到使用“单子(monads)”来保存状态的示例。
2. 第 1 天:逻辑基础
我们使用 GHC(Glasgow Haskell Compiler)6.12.1 版本开始学习,在控制台输入
ghci
启动:
GHCi, version 6.12.1: http://www.haskell.org/ghc/ :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
启动后就可以输入命令进行操作。
2.1 表达式和基本类型
我们先从基本类型开始学习,包括数字、字符数据和布尔值。
- 数字 :和其他语言一样,我们可以输入一些简单的表达式:
Prelude> 4
4
Prelude> 4 + 1
5
Prelude> 4 + 1.0
5.0
Prelude> 4 + 2.0 * 5
14.0
Prelude> 4 * 5 + 1
21
Prelude> 4 * (5 + 1)
24
可以看到,运算顺序和预期一致,并且可以使用括号来分组运算。
-
字符数据
:字符串用双引号表示,字符串拼接使用
++而不是+:
Prelude> "hello"
"hello"
Prelude> "hello" + " world"
<interactive>:1:0:
No instance for (Num [Char])
arising from a use of `+' at <interactive>:1:0-17
Possible fix: add an instance declaration for (Num [Char])
In the expression: "hello" + " world"
In the definition of `it': it = "hello" + " world"
Prelude> "hello" ++ " world"
"hello world"
单个字符用单引号表示,字符串实际上是字符列表:
Prelude> 'a'
'a'
Prelude> ['a', 'b']
"ab"
- 布尔值 :布尔值是另一种基本类型,相等和不相等表达式返回布尔值:
Prelude> (4 + 5) == 9
True
Prelude> (5 + 5) /= 10
False
在 Haskell 中,
if
是一个函数,而不是控制结构,它返回一个值。注意,Haskell 是强类型语言,
if
语句的条件必须是布尔类型:
Prelude> if (5 == 5) then "true"
<interactive>:1:23: parse error (possibly incorrect indentation)
Prelude> if (5 == 5) then "true" else "false"
"true"
Prelude> if 1 then "true" else "false"
<interactive>:1:3:
No instance for (Num Bool)
arising from the literal `1' at <interactive>:1:3
...
Prelude> "one" + 1
<interactive>:1:0:
No instance for (Num [Char])
arising from a use of `+' at <interactive>:1:0-8
...
我们可以使用
:t
查看 Haskell 的类型推断结果:
Prelude> :set +t
Prelude> 5
5
it :: Integer
Prelude> 5.0
5.0
it :: Double
Prelude> "hello"
"hello"
it :: [Char]
Prelude> (5 == (2 + 3))
True
it :: Bool
2.2 函数
函数是 Haskell 编程范式的核心,由于 Haskell 具有强静态类型,每个函数通常由可选的类型声明和实现两部分组成。
-
定义基本函数
:在控制台中,我们可以使用
let绑定变量和函数:
Prelude> let x = 10
Prelude> x
10
Prelude> let double x = x * 2
Prelude> double 2
4
在编写 Haskell 模块时,函数定义如下:
-- double.hs
module Main where
double x = x + x
加载模块并使用函数:
Prelude> :load double.hs
[1 of 1] Compiling Main
( double.hs, interpreted )
Ok, modules loaded: Main.
*Main> double 5
10
我们也可以为函数添加类型声明:
-- double_with_type.hs
module Main where
double :: Integer -> Integer
double x = x + x
加载并使用:
[1 of 1] Compiling Main
( double_with_type.hs, interpreted )
Ok, modules loaded: Main.
*Main> double 5
10
*Main> :t double
double :: Integer -> Integer
没有类型声明时,函数的类型会更通用:
*Main> :t double
double :: (Num a) => a -> a
- 递归 :我们可以使用递归实现阶乘函数:
Prelude> let fact x = if x == 0 then 1 else fact (x - 1) * x
Prelude> fact 3
6
使用模式匹配可以让代码更简洁:
-- factorial.hs
module Main where
factorial :: Integer -> Integer
factorial 0 = 1
factorial x = x * factorial (x - 1)
也可以使用守卫来实现:
-- fact_with_guard.hs
module Main where
factorial :: Integer -> Integer
factorial x
| x > 1 = x * factorial (x - 1)
| otherwise = 1
-
元组和列表 :我们以斐波那契数列为例,展示元组和列表的使用。
-
简单斐波那契数列 :
-- fib.hs
module Main where
fib :: Integer -> Integer
fib 0 = 1
fib 1 = 1
fib x = fib (x - 1) + fib (x - 2)
- 使用元组优化斐波那契数列 :
-- fib_tuple.hs
module Main where
fibTuple :: (Integer, Integer, Integer) -> (Integer, Integer, Integer)
fibTuple (x, y, 0) = (x, y, 0)
fibTuple (x, y, index) = fibTuple (y, x + y, index - 1)
fibResult :: (Integer, Integer, Integer) -> Integer
fibResult (x, y, z) = x
fib :: Integer -> Integer
fib x = fibResult (fibTuple (0, 1, x))
运行结果:
*Main> fib 100
354224848179261915075
*Main> fib 1000
43466557686937456435688527675040625802564660517371780
40248172908953655541794905189040387984007925516929592
25930803226347752096896232398733224711616429964409065
33187938298969649928516003704476137795166849228875
- 使用元组和函数组合 :我们可以通过组合函数来实现一些功能,例如计算列表的第二个元素:
*Main> let second = head . tail
*Main> second [1, 2]
2
*Main> second [3, 4, 5]
4
另一种斐波那契数列的实现:
-- fib_pair.hs
module Main where
fibNextPair :: (Integer, Integer) -> (Integer, Integer)
fibNextPair (x, y) = (y, x + y)
fibNthPair :: Integer -> (Integer, Integer)
fibNthPair 1 = (1, 1)
fibNthPair n = fibNextPair (fibNthPair (n - 1))
fib :: Integer -> Integer
fib = fst . fibNthPair
- 遍历列表 :我们可以使用模式匹配将列表拆分为头和尾:
let (h:t) = [1, 2, 3, 4]
*Main> h
1
*Main> t
[2,3,4]
定义列表的
size
和
prod
函数:
-- lists.hs
module Main where
size [] = 0
size (h:t) = 1 + size t
prod [] = 1
prod (h:t) = h * prod t
使用
zip
函数组合列表:
*Main> zip "kirk" "spock"
[('k','s'),('i','p'),('r','o'),('k','c')]
Prelude> zip ["kirk", "spock"] ["enterprise", "reliant"]
[("kirk","enterprise"),("spock","reliant")]
2.3 生成列表
我们可以通过递归、范围和列表推导式来生成新的列表。
-
递归
:使用
:运算符组合头和尾来构建列表:
Prelude> let h:t = [1, 2, 3]
Prelude> h
1
Prelude> t
[2,3]
Prelude> 1:[2, 3]
[1,2,3]
定义返回列表中偶数的函数:
-- all_even.hs
module Main where
allEven :: [Integer] -> [Integer]
allEven [] = []
allEven (h:t) = if even h then h:allEven t else allEven t
- 范围和组合 :Haskell 支持范围表示,默认增量为 1:
Prelude> [1..2]
[1,2]
Prelude> [1..4]
[1,2,3,4]
Prelude> [10..4]
[]
Prelude> [10, 8 .. 4]
[10,8,6,4]
Prelude> [10, 9.5 .. 4]
[10.0,9.5,9.0,8.5,8.0,7.5,7.0,6.5,6.0,5.5,5.0,4.5,4.0]
Prelude> take 5 [ 1 ..]
[1,2,3,4,5]
Prelude> take 5 [0, 2 ..]
[0,2,4,6,8]
- 列表推导式 :列表推导式可以方便地构建和转换列表:
Prelude> [x * 2 | x <- [1, 2, 3]]
[2,4,6]
Prelude> [ (y, x) | (x, y) <- [(1, 2), (2, 3), (3, 1)]]
[(2,1),(3,2),(1,3)]
Prelude> [ (4 - x, y) | (x, y) <- [(1, 2), (2, 3), (3, 1)]]
[(3,2),(2,3),(1,1)]
Prelude> let crew = ["Kirk", "Spock", "McCoy"]
Prelude> [(a, b) | a <- crew, b <- crew]
[("Kirk","Kirk"),("Kirk","Spock"),("Kirk","McCoy"),
("Spock","Kirk"),("Spock","Spock"),("Spock","McCoy"),
("McCoy","Kirk"),("McCoy","Spock"),("McCoy","McCoy")]
Prelude> [(a, b) | a <- crew, b <- crew, a /= b]
[("Kirk","Spock"),("Kirk","McCoy"),("Spock","Kirk"),
("Spock","McCoy"),("McCoy","Kirk"),("McCoy","Spock")]
Prelude> [(a, b) | a <- crew, b <- crew, a < b]
[("Kirk","Spock"),("Kirk","McCoy"),("McCoy","Spock")]
3. 与 Philip Wadler 的访谈
我们采访了参与设计 Haskell 的 Philip Wadler,了解了 Haskell 的设计初衷、优点、改进方向以及有趣的应用案例。
-
设计初衷 :在 20 世纪 80 年代末,有许多不同的团队在设计和实现函数式语言,大家意识到合作会更强大。最初的目标是让 Haskell 成为研究的基础、适合教学并能用于工业用途。
-
优点 :Philip 喜欢使用列表推导式,认为它很方便,并且已经被其他语言采用。类型类提供了简单的泛型编程形式,只需添加一个关键字
derived就可以获得比较值、转换值等功能。Haskell 还非常适合嵌入其他特定领域的编程语言,其惰性、lambda 表达式、单子和箭头符号、类型类、表达性强的类型系统和模板 Haskell 都支持以各种方式扩展语言。 -
改进方向 :随着分布式变得越来越重要,需要关注在多台机器上运行的程序,发送值时可能希望是值本身(急切求值),而不是一个可以求值得到该值的程序。因此,在分布式环境中,默认采用急切求值,但在需要时可以方便地使用惰性求值会更好。
-
有趣的应用案例 :Haskell 在自然语言处理、蛋白质折叠、金融、代码更新工具以及垃圾收集等领域都有应用。
4. 第 1 天学习总结
第 1 天我们学习了 Haskell 的基本表达式、简单数据类型、函数定义、递归、元组和列表的使用,以及列表的生成方法。通过这些学习,我们对 Haskell 的基本特性有了初步的了解。
5. 第 1 天自我学习任务
为了巩固所学知识,我们可以完成以下自我学习任务:
-
查找资料
:
- 查找 Haskell 维基百科。
- 找到支持你所使用编译器的 Haskell 在线社区。
-
实践任务
:
- 尝试用不同的方法实现
allEven
函数。
- 编写一个函数,将列表反转。
- 编写一个函数,生成包含黑色、白色、蓝色、黄色和红色中任意两种颜色的所有可能组合的二元组,且只包含
(black, blue)
和
(blue, black)
中的一个。
- 编写一个列表推导式,生成一个乘法表,乘法表是一个三元组列表,前两个元素是 1 到 12 的整数,第三个元素是前两个元素的乘积。
- 使用 Haskell 解决地图着色问题。
通过这些任务,我们可以进一步熟悉 Haskell 的语法和特性,提高编程能力。
下面是一个简单的流程图,展示了我们第 1 天学习 Haskell 的主要步骤:
graph TD;
A[启动 GHCi] --> B[学习基本表达式和类型];
B --> C[学习函数定义];
C --> D[学习递归和元组列表];
D --> E[学习列表生成方法];
E --> F[完成自我学习任务];
通过今天的学习,我们已经迈出了学习 Haskell 的第一步,后续我们将继续深入学习 Haskell 的高级特性,如参数化类型系统和单子。希望大家在学习过程中不断实践,加深对 Haskell 的理解。
Haskell 入门:第 1 天学习指南
6. 关键知识点总结
为了更好地回顾第 1 天所学内容,下面通过表格形式总结关键知识点:
| 知识点 | 描述 | 示例代码 |
| — | — | — |
| 基本类型 | 包括数字、字符数据、布尔值 | 数字:
4 + 1
;字符数据:
"hello" ++ " world"
;布尔值:
(4 + 5) == 9
|
| 函数定义 | 由可选类型声明和实现组成 |
double :: Integer -> Integer; double x = x + x
|
| 递归 | 实现阶乘、斐波那契数列等 |
factorial x = if x == 0 then 1 else x * factorial (x - 1)
|
| 元组和列表 | 用于存储数据和优化算法 | 元组:
fibTuple (x, y, index) = fibTuple (y, x + y, index - 1)
;列表:
size (h:t) = 1 + size t
|
| 列表生成 | 递归、范围、列表推导式 | 递归:
allEven (h:t) = if even h then h:allEven t else allEven t
;范围:
[1..4]
;列表推导式:
[x * 2 | x <- [1, 2, 3]]
|
7. 自我学习任务解析
下面对第 1 天的自我学习任务进行详细解析,帮助大家更好地完成任务。
7.1 用不同方法实现
allEven
函数
除了之前使用的递归方法,还可以使用列表推导式实现:
-- all_even_alt.hs
module Main where
allEven :: [Integer] -> [Integer]
allEven lst = [x | x <- lst, even x]
7.2 编写列表反转函数
可以使用递归和
foldl
函数实现列表反转:
-- reverse_list.hs
module Main where
-- 递归实现
reverseRecursive :: [a] -> [a]
reverseRecursive [] = []
reverseRecursive (x:xs) = reverseRecursive xs ++ [x]
-- foldl 实现
reverseFoldl :: [a] -> [a]
reverseFoldl = foldl (flip (:)) []
7.3 生成颜色组合二元组
-- color_combinations.hs
module Main where
colors = ["black", "white", "blue", "yellow", "red"]
colorCombinations :: [(String, String)]
colorCombinations = [(a, b) | a <- colors, b <- colors, a < b]
7.4 生成乘法表
-- multiplication_table.hs
module Main where
multiplicationTable :: [(Integer, Integer, Integer)]
multiplicationTable = [(x, y, x * y) | x <- [1..12], y <- [1..12]]
7.5 解决地图着色问题
地图着色问题可以通过回溯算法解决。以下是一个简单示例:
-- map_coloring.hs
module Main where
-- 定义地图区域和相邻关系
regions = ["A", "B", "C", "D"]
neighbors = [("A", "B"), ("A", "C"), ("B", "C"), ("B", "D"), ("C", "D")]
colors = ["red", "green", "blue"]
-- 检查分配是否合法
isValid :: [(String, String)] -> (String, String) -> Bool
isValid assignment (region, color) = all (\(r, c) -> r /= region || c /= color) [(r, c) | (r, c) <- assignment, (r, region) `elem` neighbors || (region, r) `elem` neighbors]
-- 回溯算法
colorMap :: [(String, String)] -> [String] -> [(String, String)]
colorMap assignment [] = assignment
colorMap assignment (r:rs) = head [colorMap ((r, c):assignment) rs | c <- colors, isValid assignment (r, c)]
main :: IO ()
main = print (colorMap [] regions)
8. 学习路径规划
接下来的学习可以按照以下步骤进行,形成一个清晰的学习路径:
1.
巩固基础
:复习第 1 天所学内容,完成自我学习任务,确保对基本概念和语法有扎实的掌握。
2.
深入学习高级特性
:学习参数化类型系统和单子等高级特性,理解 Haskell 的强大之处。
3.
实践项目
:选择一些小型项目进行实践,如命令行工具、简单的 Web 应用等,将所学知识应用到实际中。
4.
参与社区
:加入 Haskell 社区,与其他开发者交流经验,学习优秀的代码和设计模式。
下面是相应的流程图:
graph LR;
A[巩固基础] --> B[深入学习高级特性];
B --> C[实践项目];
C --> D[参与社区];
9. 总结与展望
通过第 1 天的学习,我们对 Haskell 这门纯粹的函数式语言有了初步认识。从基本表达式、函数定义到递归、元组和列表的使用,再到列表生成方法,每个知识点都为我们打开了 Haskell 编程的一扇窗。
在后续的学习中,我们将面临更具挑战性的内容,如参数化类型系统和单子。这些高级特性虽然理解起来有一定难度,但它们是 Haskell 强大功能的核心所在。希望大家保持积极的学习态度,不断实践,逐步掌握 Haskell 的精髓,在函数式编程的世界中探索更多的可能性。
让我们继续努力,在 Haskell 的学习道路上不断前进!
超级会员免费看
113

被折叠的 条评论
为什么被折叠?



