REPL
之前我们已经介绍过,可以通过 ghci
命令进入
REPL。这里介绍一些基础 GHCi 命令:
:quit
:q
用于退出:info
:i
用于查询类型信息,比如:i Int
:load
用于加载文件。Haskell 后缀为.hs
。可以创建一个test.hs
文件::reload
:r
用于重新加载相同的文件
sayHello :: String -> IO ()
=
sayHello x putStrLn ("Hello, " ++ x ++ "!")
使用 :load test.hs
加载后交互:
Prelude> sayHello "Haskell"
Hello, Haskell!
表达式
在 Haskell 中的一切,都是表达式或声明。表达式可以是值、值的组合、已应用的函数。表达式会求出一个结果。若表达式是字面量,求值微不足道,因为结果就是其自身。在涉及计算时,求值过程是计算运算符和其参数的过程。Haskell 表达式以可预测的、透明的方式求值。整个程序也是由较小表达式构成的一个大表达式。
而声明允许我们命名表达式,并使用它们,并多次引用,而无需复制。
以下这些都是表达式:
1
1 + 1
"Hello"
1 + 2) * 3) + 100 ((
当没有更多求值步骤可以执行时,亦即,达到不可归约形式时,我们说表达式是标准形式的。如 \(1 + 1\) 的不可归约形式是 \(2\).
函数
表达式是 Haskell 程序最基本的单元,而函数是其中的一种。
之前已经说过,函数只能有一个实参,并且返回一个结果。当想要有多个形参时,需要柯里化对应函数。
函数可以像这样定义:
= x * 3 triple x
并使用:
114514 triple
请注意,这里虽然没有写类型。但并不代表 Haskell 类似 Python 甚至 JavaScript 是动态类型甚至弱类型。这是 Haskell 强大的类型推断所做的,甚至可以推断函数的形参类型和返回值。
不妨使用 :info triple
查看:
Prelude> :info triple
triple :: Num a => a -> a
求值
对表达式求值,就是减少项直到最简形式的过程。一旦项到达最简形式,就可称其为不可归约(irreducible)或完成求值。
Haskell 使用非严格求值(non-strict evaluation)策略,有时候也称惰性求值(lazy evaluation)策略,该策略推迟项的求值,直到它切实所需。
值是不可约的,但函数对参数的应用是可约的。减少一个表达式意味着求值项。与 lambda 演算一样,应用就是求值:将函数应用于参数允许求值或归约。
值是无法再被归约的表达式,也是归约的终点。
Haskell 默认不会求值,而是尝试求出标准形式的宽松版本,即 WHNF(weak head normal form)。
比如,这个表达式:
-> (1, 2 + f)) 2 (\f
归约为 WHNF:
1, 2 + 2) (
这种表达是一种近似,但关键点在于,2 + 2
没有被求为
4.
中缀操作符
Haskell 的函数默认为前缀语法,也就是说,函数位于表达式的开头,而不是中间:
Prelude> id 1
1
id
函数就是 \(\lambda x.x\), 亦即 identity(恒等式)。
这也是函数的默认语法,但不是所有函数都是前缀的。有一些函数默认为中缀,比如算术操作符。
操作符(operators)是可以被中缀形式调用的函数。所有操作符都是函数;但不是所有函数都是操作符。triple
和 id
是前缀的函数(不是操作符),而 +
函数之类的是中缀操作符:
Prelude> 1 + 1
2
Prelude> 100 + 100
200
Prelude> 768395 * 21356345
16410108716275
Prelude> 123123 / 123
1001.0
Prelude> 476 - 36
440
Prelude> 10 / 4
2.5
为函数套上反引号,就可以用中缀形式使用它们:
Prelude> div 10 4
2
Prelude> 10 `div` 4
2
为中缀操作符套上括号,就可以用前缀形式使用它们:
Prelude> (+) 100 100
200
Prelude> (*) 768395 21356345
16410108716275
Prelude> (/) 123123 123
1001.0
若函数是字母和数字组成的,那么默认为前缀,并且不是所有函数,都可以套一层反引号变为中缀形式(显然的,实参数被限定为 2);若名称是单个符号,那么默认为中缀,并且肯定可以套一层括号变为前缀。
结合性和优先级
对于中缀表达式,结合性(associativity)和优先级(precedence)是重要的。
我们可以通过 :i 操作符
查看相关信息:
:info (*)
infixl 7 *
-- [1] [2] [3]
:info (+) (-)
infixl 6 +
infixl 6 -
infixl
表示,这是一个中缀操作符;其中l
表示left
,即左结合性。- 比如
2 * 3 * 4
,因为*
是左结合的,所以会解析为(2 * 3) * 4
- 比如
7
是优先级:越大优先级越高,越先应用,范围为0-9
- 中缀函数名:这个例子中是乘法。
Prelude> :info (^)
infixr 8 ^
infixr
表示,这是一个中缀操作符;其中r
表示right
,即右结合性。8
是优先级:幂运算比加减(6)、乘除(7)都高。
因为 ^
是右结合的,所以它的表现类似这样:
Prelude> 2 ^ 3 ^ 4
2417851639229258349412352
Prelude> 2 ^ (3 ^ 4)
2417851639229258349412352
Prelude> (2 ^ 3) ^ 4
4096
通常而言,数学中的优先级对 Haskell 也适用:
2 + 3 * 4
2 + 3) * 4 (
声明值
可以用 =
声明值:
= 10
y = 10 * 5 + y
x = x * 5 myResult
值得注意的是,在文件中,声明的顺序不重要,因为它们被同时读取。但在 REPL 中,需要按照先后顺序声明。
-- learn.hs
module Learn where
= 10 * 5 + y
x
= x * 5
myResult
= 10 y
模块名使用 CamelCase,变量名使用 camelCase。
缩进
Haskell 不使用大括号或分号来维护代码结构,而是使用缩进。另外,Haskell 建议使用空格(通常每一级两个空格)而不是 Tab。
以下代码是合法的:
=
foo x let y = x * 2
= x ^ 2
z in 2 * y * z
或这样:
=
foo x let
= x * 2
y = x ^ 2
z in 2 * y * z
但这样就会报错:
=
foo x let
= x * 2
y = x ^ 2
z in 2 * y * z
这样也会:
=
foo x let y = x * 2
= x ^ 2
z in 2 * y * z
这样会报错:
= 4
y = 10
x * 5 + y
但这样就不会:
= 4
y = 10
x * 5 + y
这样也是不行的:
= 4
y = 10 * 5 + y x
总之,同一级的代码,应该使用同样的缩进。而若要换行书写表达式,则需要另加一层缩进。
算术函数
操作符 | 名称 | 目的 |
---|---|---|
+ | plus | 加 |
- | minus | 减 |
* | asterisk | 乘 |
/ | slash | 小数除法 |
div | divide | 整数除法,向下取整 |
mod | modulo | 类似 rem ,但是经过模数除法 |
quot | quotient | 整数除法,向零取整 |
rem | remainder | 除数的余数 |
通常而言,整数除法使用 div
,而非
quot
,因为它们的取整方式不同:
-- 向下取整
div 20 (-6) -- Result: -4
-- 向 0 取整
quot 20 (-6) -- Result: -3
另外,rem
和 mod
也有微小的不同……
商余法则
商余法则即:
quot x y) * y + (rem x y) == x
(div x y) * y + (mod x y) == x (
这里就不必证明了,它们切实为真:
quotRem x y = (quot x y) * y + (rem x y) == x
divMod x y = (div x y) * y + (mod x y) == x
quotRem (-10) (-9) -- True
quotRem 10 (-9) -- True
quotRem 10 123 -- True
divMod 10 13 -- True
divMod (-3) 12 -- True
divMod (-3) (-12) -- True
使用 mod
之前提到,mod
返回模数除法(modular
division)的余数。若不熟悉模数除法,那么大概率是不知道 rem
和 mod
有何区别的。
模数除法是关于整数的算术系统,数字在达到某个值时「回绕」,该值亦称模数(modulus)。常见的生活对应物是时钟:
若使用 12 小时制,则需要每十二个数回绕一次。例如,现在是 8:00,你想知道 8 个小时后的时间,你并不能简单地将 8 和 8 加和,得出 16:00 的结果。
需要先 +4 然后回绕为 0。再加剩余的 4 小时。得出 4:00 的结果。
此处的算术模(arithmetic modulo)为 12. 因此,实质上在该算术系统中,\(0 = 12\)。
mod 21 12 -- 9
rem 21 12 -- 9
mod 3 12 -- 3
rem 3 12 -- 3
假设我们要编写一个判断某一星期过了多少天,是星期几的函数:
mod (1 + 23) 7 -- 3
mod (6 + 5) 7 -- 4
rem (1 + 23) 7 -- 3
到目前为止,似乎 mod
rem
区别不大。但不妨看看,被余数是负数的情况:
mod (3 - 12) 7 -- 5
rem (3 - 12) 7 -- -2
在这里,mod
是正确的。至少在 Haskell
中,mod
和 rem
具有区别:
mod
结果的符号跟随除数(divisor)rem
结果的符号跟随被除数(dividend)
-5) `mod` 2 -- 1
(5 `mod` (-2) -- -1
-5) `mod` (-2) -- -1
(
-5) `rem` 2 -- -1
(5 `rem` (-2) -- 1
-5) `rem` (-2) -- -1 (
负数
为了更好地和括号、柯里化、中缀语法交互,负数在 Haskell 被特殊对待。
单独使用负号没有问题:
Prelude> -1000
-1000
然而组合使用就会出问题:
Prelude> 1000 + -9
<interactive>:3:1:
Precedence parsing error
cannot mix ‘+’ [infixl 6] and
prefix `-` [infixl 6]
in the same infix expression
我们需要使用括号包裹:
Prelude> 1000 + (-9)
991
在 Haskell 中,将 -
作为一元(unary)操作符是一种语法糖(syntactic sugar)。-
在 Haskell 中有两种意义:1. 减法操作符 2. negate
的别名。以下代码的语义是完全相等的:
Prelude> 2000 + (-1234)
766
Prelude> 2000 + (negate 1234)
766
当 -
用作减法:
Prelude> 2000 - 1234
766
括号
本节将解释 $
。
ghci> :info $
($) :: (a -> b) -> a -> b -- Defined in ‘GHC.Base’
infixr 0 $
可以看到,($)
的优先级是最低的 0。
并且定义也很简单:
$ a = f a f
乍一看,这似乎没有什么意义,但别忘了,以符号为名的函数,默认为中缀操作符。我们可以使用
($)
操作符减少括号:
Prelude> (2^) (2 + 2)
16
Prelude> (2^) $ 2 + 2
16
Prelude> (2^) 2 + 2
6
该操作符允许它右边的所有东西先被求值,这对延迟函数应用很有用。
在同一个表达式中,可以使用多个 ($)
:
Prelude> (2^) $ (+2) $ 3 * 2
256
但这样不行:
Prelude> (2^) $ 2 + 2 $ (*30)
因为 ($)
的右结合性,我们必须从最右侧开始归约:
2^) $ (*30) $ 2 + 2
(-- 首先求值最右侧
2^) $ (*30) $ 2 + 2
(-- 要想将函数 (*30) 应用于表达式 (2 + 2)
-- 必须先求出表达式的值
2^) $ (*30) 4
(-- 然后再归约 (*30) 4
2^) $ 120
(-- 归约 ($)
2^) 120
(-- 归约 (2^)
1329227995784915872903807060280344576
此外,(*30)
这种写法被称为分片(sectioning)。
括号化中缀操作符
有些时候,你想引用中缀函数而不应用任何实参;也有些时候,你想将它们当作前缀运算符使用。这两种情况下,都必须用圆括号把运算符包起来:
1 + 2 -- 3
+) 1 2 -- 3
(+1) 2 -- 3 (
最后一种写法即为分片。
对于满足交换律(commutative)的函数,例如
(+)
,(+1)
和 (1+)
没有区别。但若函数不满足交换律,则需要小心:
1/) 2 -- 0.5
(/1) 2 -- 2.0 (
减法,(-)
作为特例,这是正常的:
2 - 1 -- 1
-) 2 1 -- 1 (
但这不行:
-2) 1 -- error (
将 -
包裹在括号中,让 GHCi 认为这是函数的参数。因为
-
意味着相反数 negate
,而不是减法。所以 GHCi
不知道下一步要做什么,因此返回了错误信息。
你可以对于减法使用分片,但它必须是第一个实参:
= 5
x = (1 -)
y -- -4 y x
或者,你可以这样,使用 subtract
而非
-
:
subtract 2) 3 -- 1 (
let
和 where
let
和 where
经常用于引入表达式的组成部分。不同的是,let
引入了一个表达式,所以它可以在任何可以有表达式的地方使用;而
where
是一个声明,并受限于上下文。
一个简单的 where
例子:
-- FunctionWithWhere.hs
module FunctionWithWhere where
= print plusTwo
printInc n where
= n + 2 plusTwo
在 REPL 中使用:
Prelude> :l FunctionWithWhere.hs
Prelude> printInc 1
3
同样函数的 let
写法:
-- FunctionWithLet.hs
module FunctionWithLet where
=
printInc2 n let plusTwo = n + 2
in print plusTwo
若 let
后面跟着 in
,那么该 let
就用作表达式。在 REPL 中使用:
Prelude> :load FunctionWithLet.hs
Prelude> printInc2 3
5
结
以上就是基本的 Haskell 语法。