A Quick Tour of Haskell Syntax
原文:https://prajitr.github.io/quick-haskell-syntax/
如果你想解析 Haskell 来完成一篇博文,或者想快速了解 Haskell 的大概,那本文就是你要找的
比起“在X分钟学习Y”类文章,本文可能稍长一点,而且会深入某些内容。
话不多说,我们开始吧
数据类型
我们从基本数据类型开始:
1 | -- inline comment |
上面的 String 其实是字符的列表,因此,”Hello” 与 [‘H’,’e’,’l’,’l’,’o’] 是相同的
函数
接着,是基本函数:
1 | 7 + 12 -- 19 |
not 是一个函数,它接受一个参数并取反
Haskell 中的函数一般不需要小括号调用,就像这样: func arg1 arg2 arg3 ...
,不过,有些情况小括号是必须的,如 not (3 < 5)
计算出 False,而 not 3 < 5
则是错误的
11 `div` 8 等价于 div 11 8,将函数名放在一对反引号内允许你以中缀风格调用。考虑到有些函数采用中缀调用可读性更好,这只是由此产生的一种语法糖
以下是用于列表的函数:
1 | 1 : [2, 3] -- [1, 2, 3] |
1 : 2 : 3 : [] 和 [1, 2, 3] 是一样的,与 lisp 一样,列表就是元素连接到一起,最终连接到一个空列表([])
(elem1, elem2, elem3, …) 称为元组,它可以保存不同类型的元素,而列表只能保存同一类型元素
定义函数
来看如何定义函数:
1 | id :: a -> a |
该函数接受一个参数,并直接返回
第一行是类型声明,从左到右依次是函数名(id),两个冒号,最后是类型签名。
这里类型签名的含义是:接受一个 a 类型(a 指代任意类型)参数,返回一个 a 类型的值
第二行是 id 的计算过程,它取出参数然后不加修改地返回它
为了更好地理解,再看一个例子:
1 | const :: a -> b -> a |
该函数接收两个参数,返回第一个参数,忽略第二个参数
类型声明的含义是:取得任意类型 a 的值,取出任意类型 b 的值(实际类型可以与 a 相同或不相同),最后返回一个 a 类型的值
最后一个 -> 后面的类型为返回值类型,前面的类型都是参数类型,均以 -> 分隔
以下是一个稍复杂的例子:
1 | concatenate3 :: String -> String -> String -> String |
concatenate3 接收三个 String 参数,返回一个 String,这里 String 对比前一个例子的泛型参数 a b,为具体类型
可以在类型签名中混合具体类型和泛型类型
在 allEqual 的类型签名中,我们看到 (Eq a),它代表限制 a 的范围为能够比较相等与否的任意类型。a 可以被加以多个限制:(Eq a, Ord a, Num b, …) =>
匿名函数
使用 Haskell 创建匿名函数很容易:
1 | (\x -> x + x) |
匿名函数用小括号括起来,在小括号内以 \ 开始,接着是空格隔开的参数列表,接着是 -> 符号,最后是计算过程
函数是一等公民
在 Haskell 中,函数是一等公民,可以当做参数传递,从函数返回,从变量赋值,被数据结构(如列表)拥有
1 | applyAndConcat :: (String -> String) -> (String -> String) -> String -> String |
applyAndConcat 接收两个函数,和一个 String,将两个函数分别应用至 String 后连接两个结果并返回
函数复合
1 | (.) :: (b -> c) -> (a -> b) -> (a -> c) |
. 函数称为函数复合,类似数学中的 fog 记号。它接收两个函数 f, g,返回一个函数,该函数将参数传给 g(即调用 g),然后将结果传给 f(即调用 f)。
$函数
1 | ($) :: (a -> b) -> a -> b |
$ 符号经常出现在 Haskell 中,乍一看,$ 好像没有任何作用
大多时候,$ 并不用于计算,而是为了减少括号的数量,例如:not (3 < 5) 可以写作 not $ 3 < 5,它仅仅实现一种风格,并且,它不是什么特殊的操作符,而是一个普普通通的函数
部分应用
1 | add x y = x + y |
在 Haskell 中,你可以部分应用一个函数,也就是说,对于接收 n 个参数的函数,你可以部分应用 k (k < n)个参数,最终产生一个接收 n-k 个参数的函数
可以看到,类型签名是可以省略的,如果省略了类型参数,编译器会自动推出类型,带来灵活性
局部变量
1 | addFour w x y z = |
let … in … 和 … where … 可以让你构建计算并把结果保存至局部变量,这两种形式功能完全一致,如何选择只看个人喜好
模式匹配
以下是斐波那契数列典型递归实现:
1 | fib 0 = 1 |
其中,fib 0 = 1, fib 1 = 1 属于模式匹配,Haskell 会从上往下遍历列表试图找到一个匹配的定义。模式匹配在 Haskell 中非常重要,你将经常看到它
1 | fib n |
以上仅是使用不同语法实现的 fact 函数变体
第一个称作 guard expressions,用 | 列出 n 的不同情况及对应值。otherwise 能匹配所有情况(事实上它和 True 一致)
case … of … 也是模式匹配,不同的是,你还可以在计算过程中使用它
if … then … else … 就是常规的 if 语句了,Haskell 中,每个 if 都需要对应的 else 部分
Haskell必备函数
我们来定义一些必备的 Haskell 函数:
1 | map :: (a -> b) -> [a] -> [b] |
map 接收一个函数和一个列表,并将函数应用至列表每个元素
第二行中的 _ 代表我们忽略该参数,它不是必要的,但可以让代码的意图更明显
(x:xs) 是将列表第一个元素(x)和其余元素(xs)分开来的模式匹配记号
1 | filter :: (a -> Bool) -> [a] -> [a] |
filter 与 map 类似,但它接受一个返回 Bool 的函数,用于保留列表中满足条件的元素
还有更多列表相关函数,例如 foldr,对它的了解及其实现就留作读者做练习吧
数据类型
让我们转到类型话题,有三种创建类型的方式:
类型同义词
1 | type Email = String |
第一种称为“类型同义词”,type 的后面是类型名(必须大写字母开头),然后是一个 = 号,最后是一个已定义类型
类型同义词其中一个意义是为类型签名提供文档,其二是为复杂类型提供一个简单的别名,以减少输入字符数
类型同义词与它所代表的类型可以交替使用,如果你认为它没用,只需记住 type String = [Char]
代数数据类型
1 | data Bool = True | False |
第二种是代数数据类型,让我们能够创建全新的类型
所有 data 和 = 之间的东西都是类型签名的一部分, = 后面的部分为值构造器(value constructors),会成为函数计算的一部分
| 的作用与“或”差不多:一个布尔值可以为 True 或 False。Bool 永远是类型签名的一部分,计算过程中,你会使用的是 True,False
在看Maybe String, Maybe Int,Maybe a,它们都是有效的,如果只有 Maybe 则是无效的,data May a = … 中的 a 被称为类型参数,类型参数让类型成为多态的(即:同时支持Maybe String,Maybe Int)。任何值构造器名(Just a 中的 Just)后面的值都是字段。每个值构造器所拥有的字段数不需要相等,可以看到,Nothing 没有任何字段,而 Just 有一个类型为 a 的字段
1 | data Tree a = Empty | Node (Tree a) a (Tree a) |
代数数据类型还可以自引用。Node 有三个字段,其中两个都是 Tree a, Tree 会由 Node 构造起来,而 Empty 会是每个分叉的末端。
第二个 Tree 的定义使用了记录语法,记录语法可以取代模式匹配去访问 Node 的字段:传递 Node 调用自动生成的 left 函数即可访问左子树, 记录语法让我们更便捷地操作值构造器
newtype
第三种是 newtype
1 | newtype State s a = State { runState :: s -> (s, a) } |
newtype 可以产生其他类型的轻度包装,它只能有一个包含一个字段的值构造器。在处理 typeclass 时,newtype将非常有用。
接下来就介绍 typeclass。
typeclass
typeclass 类似 interface,它定义类型必须遵守的行为的集合。例如:如果一个类型是 Eq 这个 typeclass 实例,那就必须实现 == 或 /=,用以比较相等性
1 | data Rectangle = Rect Int Int |
以上定义了一个类型 Rect,然后通过比较两个 Rect 的字段让它成为 Eq 的实例
如同前面见过的,我们可以在类型签名中使用 typeclass 以声明类型必须拥有的行为
结束
好了,以上就是本教程所有内容。该教程节奏可能偏快,如果你有不懂的地方,可能还需要自己动手搜索一下。一些好的资源包括:Learn You a Haskell, Real World Haskell, 和 Hoogle,Hoogle允许你通过函数名(如:map)或者它的类型签名搜索((a -> b) -> [a] -> [b]
)。希望在它们的帮助下,你能够基本理解并阅读一些 Haskell 代码。