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 程序最基本的单元,而函数是其中的一种。
之前已经说过,函数只能有一个实参,并且返回一个结果。当想要有多个形参时,需要柯里化对应函数。
函数可以像这样定义:
triple x = x * 3并使用:
triple 114514请注意,这里虽然没有写类型。但并不代表 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)。
比如,这个表达式:
(\f -> (1, 2 + f)) 2归约为 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声明值
可以用 = 声明值:
y = 10
x = 10 * 5 + y
myResult = x * 5值得注意的是,在文件中,声明的顺序不重要,因为它们被同时读取。但在 REPL 中,需要按照先后顺序声明。
-- learn.hs
module Learn where
x = 10 * 5 + y
myResult = x * 5
y = 10模块名使用 CamelCase,变量名使用 camelCase。
缩进
Haskell 不使用大括号或分号来维护代码结构,而是使用缩进。另外,Haskell 建议使用空格(通常每一级两个空格)而不是 Tab。
以下代码是合法的:
foo x =
let y = x * 2
z = x ^ 2
in 2 * y * z或这样:
foo x =
let
y = x * 2
z = x ^ 2
in 2 * y * z但这样就会报错:
foo x =
let
y = x * 2
z = x ^ 2
in 2 * y * z这样也会:
foo x =
let y = x * 2
z = x ^ 2
in 2 * y * z这样会报错:
y = 4
x = 10
* 5 + y但这样就不会:
y = 4
x = 10
* 5 + y这样也是不行的:
y = 4
x = 10 * 5 + y总之,同一级的代码,应该使用同样的缩进。而若要换行书写表达式,则需要另加一层缩进。
算术函数
| 操作符 | 名称 | 目的 |
|---|---|---|
| + | 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。
并且定义也很简单:
f $ a = f a乍一看,这似乎没有什么意义,但别忘了,以符号为名的函数,默认为中缀操作符。我们可以使用
($) 操作符减少括号:
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
不知道下一步要做什么,因此返回了错误信息。
你可以对于减法使用分片,但它必须是第一个实参:
x = 5
y = (1 -)
y x -- -4或者,你可以这样,使用 subtract 而非
-:
(subtract 2) 3 -- 1let 和 where
let 和 where
经常用于引入表达式的组成部分。不同的是,let
引入了一个表达式,所以它可以在任何可以有表达式的地方使用;而
where 是一个声明,并受限于上下文。
一个简单的 where 例子:
-- FunctionWithWhere.hs
module FunctionWithWhere where
printInc n = print plusTwo
where
plusTwo = n + 2在 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 语法。
