Haskell 学习笔记 1:基本语法与类型类
Haskell 是一门静态类型的纯函数式编程语言,比较著名的就是它的类型系统、「纯函数」性、惰性求值。我个人觉得学习 Haskell 对学习理解其它编程语言(尤其是静态语言)很有帮助,比如 Java 中的泛型、JavaScript 中的 Promise 等等,因此在这里对自己的 Haskell 学习之旅进行一个总结。
相信大部分读者和我一样,都是从 C 系语言开始学起的,这里假设你和我一样具有一定的 C 系语言的编程基础,因此主要列举一些和 C 系语言不一样的地方。
基本语法
1. 前缀函数调用
函数都是以前缀的形式调用的,如:min 2 3
就是取函数 min
施加上两个参数 2 和 3。
有些函数以前缀形式不方便读,可以改用中缀形式表示,但要用 ``` 符号将函数包起来。
2. if
Haskell 中的 if
,else
部分不能省略。实际上,这里的 if
是像 [2, 3]
这样的「必然返回结果的表达式」,而非语句。
列表
3. 列表拼接
列表拼接使用 :
运算符将元素插入列表头部,如:
而常用的 []
形式实际上是语法糖:[1,2,3]
等同于1:2:3:[]
。也就是说,列表不是「构造」出来的,而是通过组合施加 :
得出来的。
4. 列表的比较
只要列表内的元素可以比较,那么这两个列表就能作比较。具体做法是,两个列表各自从前向后挑一个元素出来做比较,如果不相等,则以两个元素的比较结果作为列表的比较结果,否则两个列表继续挑下一个比较,重复这个过程直到最后一个元素。所有元素都相等时,两个列表相等。
注意,非空列表总比空列表更大。
5. range
Haskell 中,列表可以只给出起始元素、第二个元素和上限,让 Haskell 自己推算出来中间元素,比如:
当步长为 1 时,第二个元素可以省略,上面的式子可以简写为 [1..10]
。
需要注意的是,给出区间的上限并不一定是最后一个元素,比如:
当 Haskell 推断到元素大于给定的上限时就不会再继续推断,因此上面的列表推断到 9 就截至了。
6. 无限列表
作为一个惰性求值的语言,Haskell 支持无限长列表,比如 [13, 26..]
是合法的,在 repl 中执行这个语句会让解释器不停地推断下去。( 感觉失去控制时可以按 Ctrl-C 刹车:) )
7. 列表推导式
可以像当初写数学的集合推导式一样声明一个列表,这个特性后来还被 Python 借鉴过去了。
也可以声明两个列表的笛卡尔积:
类型系统
Haskell 作为一种静态类型的函数式编程语言,类型系统在 Haskell 中有举足轻重的地位。
类型声明
虽然 Haskell 具有类型推断的能力,但我们平时在编写 Haskell 函数时,最好还是显式地在函数声明上方声明它的类型。函数的类型声明就是要声明出这个函数的输入是什么类型、输出是什么类型,例如:
注意如果函数有多个参数,则参数之间用 ->
分隔(参数和返回值之间同样也使用 ->
分隔,我们后面会介绍为什么这样做)
类型变量
类型变量类似 Java 等语言中的泛型,可以将类型声明中的类型泛化,不限制具体的类型,比如系统自带的函数 head
:
head
的类型声明表示了它可以接受装有任何元素类型的列表,并将返回这种元素类型的值(比如传入一个 Int 列表,将返回一个 Int 值)。
类型类
类型类(typeclass)类似于 Java 中的接口概念,是定义了一组行为的接口。一个类型(type)可以是一个类型类的实例(instance),但必须要实现这个类型类所定义的行为。
一个类型类的例子是定义相等性的 Eq
。一个类型,只要它实现了 Eq
类型类,那么就可以通过 ==
运算符判断这个类型不同值的相等性:
上面的类型声明中,多了一个 =>
记号,这个叫做类型约束,表示后面的类型变量 a
必须满足 Eq a
的约束,就是说 a
的类型必须实现了 Eq
这个类型类。可以类比于 Java 泛型中的 <? implements Eq>
。
为了方便接受,我们刚才都是用 Java 中相似的概念作类比的。然而类型类的概念和 Java 中的接口还是有很大不同的:Haskell 世界中函数是一等成员,任何 Haskell 函数都可以扩展类型类的行为,类型类在定义时没有声明它的行为。此外,Java 中接口在定义之时就要在接口内声明好它的行为,之后所有实现接口的类都无一例外地要实现所有行为。而在实际开发中可能用不到接口的所有函数,那些没有用到的函数有时候就通过 return null;
的方式过掉了,这样做其实会给之后的开发埋下隐患。
常见的几个类型类
Eq 类型类
Eq
用于判断相等性和不等性,所有 Eq
的实例类型都必须实现 ==
和 /=
(不等于)两个函数。
Ord 类型类
Ord
用于判断大小,所有 Ord
的实例类型都必须实现 <
、>
、<=
、>=
等函数。
Show 类型类
Show
用于表示类型的字符串,show
函数可以取任一 Show
类型类的实例类型作为参数,返回一个表示参数值的字符串。比如:
Read 类型类
Read
类型类和 Show
正相反,read
函数可以将字符串转换为 Read
的某个实例类型:
要注意的是,read
函数需要有其它上下文(比如上面例子中的 3.8
和 [True False])才能推断出要转换到什么类型。只输入 read "4"
,haskell repl 会报错。要告诉解释器我们需要转换为什么类型,需要用到类型注解,比如:
Enum 类型类
Enum
类型类的实例都有连续顺序,它的实例类型都需要支持 succ
和 pred
函数获取每个值的后继和前驱,使得我们可以在区间中使用这些类型(这样我们才能写出 [2..10]
这样简练的表达式)。
Bounded 类型类
Bounded
类型类的实例都有一个上限和下限,分别可通过 maxBound
和 minBound
函数得到。
注意上面 minBound
和 maxBound
的类型声明都是只有一个有类型约束的值,这个值叫做多态常量(polymorphic constant)。
如果元组中项的类型都属于 Bounded
类型类的实例,那么这个元组也属于 Bounded
的实例:
Num 类型类
Num
类型类用于表示数值。只有已经属于 Show
与 Eq
的实例类型,才可以成为 Num
类型类的实例。
Floating 类型类
Floating
类型类用于存储浮点数,仅包含 Float
和 Double
两种浮点类型。
Integral 类型类
Integral
类型类仅包含整数,实例类型包含 Int
和 Integer
(无限版本的 Int
)。
参考资料:
版权声明: 本文中所有文字版权均属本人所有,如需转载请注明来源。