译者前言
译者于一年前首次于课上接触OCaml语言,但发现相关的中文教材实在是少之又少。恰逢同学结合上课内容与自身理解编写了本教程,于是便拿来翻译一二以供大家借鉴。
译者第一次做翻译工作,若有错误以及不足之处欢迎指出。
原教程链接:ocaml.gelez.xyz/
教程源代码:github.com/elegaanz/co…
编者主页:github.com/elegaanz
编者前言
此教程的目的在于,你可以在INF201(格勒诺布尔大学在大一时设立的OCaml语言的课程)这门课程与本教程中相较取其一。很多学生会因为无聊而厌烦这门课程,但事实上OCaml是一门很有趣的语言。我不知道我能不能做的更好,但是试一试总归是聊胜于无。所以本教程中仅阐述OCaml语言的基础内容,并且主要面向有Python语言基础的人群。由于本教程建立在2020年的INF201课程的基础之上,所以日后课程内容若有改动,本教程可能不会完全更新。
后文中将会使用以下协定:
1.“附加”部分是课程中未收录但很有意思的部分,如果你想深入了解一下的话建议阅读。
2.需要理解的重要概念将会标红。
复制代码
注意:
建议在阅读时能按部就班地测试作为例子的代码,并在阅读下一章前能完全理解读过的内容。同样的,如果你能自己写出相关的例子会更有助于理解和消化新的概念。
I.基础概念
1.简述ocaml
在这一章节中我将会回答以下问题:
“OCaml是什么?”以及“为什么要使用OCaml?”
如果你对这些内容并不感兴趣的话,可以直接阅读下一章。
ocaml的历史
L’lnria(法国国立计算机及自动化研究院)是法国致力于研究计算机领域的主要组织之一。在1996年,L’lnria的研究员发布了第一个版本的OCaml。并从此衍生出了其它同属语言:
Caml Light、Caml、ML。
自从OCmal成为了函数式编程语言范围中一门重要语言后,就被使用在世界范围里的各个大学及企业中。
一门函数式编程语言?
既然你正在阅读本教程,那么毫无疑问已经学习过了INF101这门课程(格勒诺布尔大学在大一时设立的Python语言的课程),并且已经通过Python语言学会了基础的编程。然而Python是一门命令式编程语言(如果你没有上过INF101的课程也不要紧,大部分的编程语言差不多都是命令式的,例如:C,C++,Java,C#等)。命令式语言有明确的步骤来解决给定的问题,但是函数式编程语言却不是这样。
事实上,在函数式编程语言中,所有的编程都围绕着函数展开(令人惊讶)。如此,我们便尝试着尽可能多的去操纵数据、去用函数解决我们的问题。具体而言,
比如说我们可以全部使用循环来解决,并用函数来替代这些循环。
我想此时你心中一定有疑问,那我们如何实现那些无法使用循环的程序呢?不用担心,我们会有另一些办法来实现相同的功效,当然还是用函数!
使用函数来解决问题对你而言可能会很陌生,并对初学者来说会有些难以理解。但这只是一个习惯上的问题,而且在某些情况下,使用函数式会比命令式更加方便(特别是操作列表的时候)。
现实生活中的OCaml
OCmal是一门可以实现真正程序的语言(幸好)。在它的使用者中我们可以看到Facebook、Jane Street(华尔街的一家企业)以及Airbus。
L’lnria也同样在使用OCmal创造出了Coq这项软件,它可以借用计算机来验证各项数学定理。
安装OCmal
如果你想在自己的个人计算机上使用的话,附录1中解释了如何安装OCmal。
或者,try.ocamlpro.com/ 这个网页也可以在线测试一些小程序。
2.ocaml上编程的第一步
你现在可以在OCmal上写属于自己的第一行代码了!首先我们要打开一个解释器(interpreter)(最好是utop
,但也可以是ocaml
或者一个在线解释器)。
在编译器中,所有的命令都要以 ;; 结尾(两个分号)。由此OCmal才能分辩哪里是命令的结尾并开始执行命令。但如果你是写在一个文件(file)中的话,一般而言是不需要的。
尝试输入一个简单的计算命令(敲击回车执行):
2 + 2;;
复制代码
通常来讲,解释器会返回以下内容:
- :int = 4
复制代码
对于目前来说,我们只关注=
右边:我们的计算式成功运行了,2加2等于4。这是一个良好的开端!你接下来可以尝试一些其它的运算符号,比如*
或者是-
。
然而,对于某些运算符来说,有些小细节希望你能知道:
-
/
用来计算商(类比Python中的//
)。所以它的结果会是一个整数。 -
mod
类比于Python中的%
用来计算余数。
毫无疑问地来讲,现在最令你感到疑惑的一定是:这些操作符并不适用于浮点数(带小数点的数字)。举个例子,如果你输入了下面这个算术式,你会得到一个错误提示:
(*从现在起,我不会再写 ;; ,但是如果需要的话,别忘了加上*)
2.8 + 0.2
复制代码
为了理解为什么我们会得到一个错误提示,我们要弄清楚表达式以及类型的定义。
表达式是计算机可以解释和计算的一段代码,由此可以计算它的值。
在OCaml中,全部或者说是几乎是全部的代码有表达式构成。
但在此,我举一个Python中的例子:
12 #这是一个表达式,其值为12
12 + 7 #这是一个表达式,其值为19(在此指出,这个表达式是由两个更简单的表达式 7 和 12 组成的。)
print("你好!") #这也是一个表达式,尽管其值为None(没有值)
a = 12 #本行不是一个表达式,"a = 12"并不是一个值, 这是一个指令。
复制代码
更通俗来讲,当你想判断眼前的语句是不是一个表达式时,不妨问问自己:
“我可以将这个值存于一个变量中吗?”
从以下三个例子中,我门可以很清楚地发现,对于前两行来说,答案是“可以”。但是对于最后一行来说并不太可行。
x = (12 + 7) #没有问题
y = (print("你好!")) #虽然这看起来有点奇怪,因为在这个变量中的值为<没有值>,但是是可行的。
z = (a = 12) #尽管在另一种情况下这看起来可能合乎逻辑, 但"a = 12"并不是一个值,我们不能将它储存在变量中。
复制代码
现在我们将讨论有关类型的问题。类型是有关表达式的一项信息,它可以告诉我们表达式中可以储存的值。这有点像数学中集合的概念。毫无疑问,我们已经见到过Python中的几种类型, 比如’int’或者是’float’或者是’str’。在OCaml中,我们也仍然会看到这些类型,但是OCaml的类型体系相较于Python中更进了一步。在OCaml中,所有的表达式都有其定义好的类型:我们称之为静态类型语言。
但所有的这些并不能解释为什么两个小数相加会导致错误。
原因是因为OCaml中的运算符号+
适用于其两侧均为int
的表达式语句中,否则不适用。
对于-
、*
、/
以及mod
也是同样的道理。对于浮点数而言,我们有不同的操作符:+.
、-.
、*.
、/.
以及mod_float
。
但是为什么要复杂化这些呢?Python即使不区分这两种操作符也运行的很好,为什么OCaml不这样做呢?
OCaml非常重视类型,它需要每时每刻都知道表达式的类型。它不能对此模糊不清。同样的,这也可以保证在运行前,代码行中没有错误。
在Python中运行以下语句:
liste = [1, 2, 3]
liste + "Oups" #使一个列表和一个文本相加??️
复制代码
在类型方面,Python并不像OCaml一样严格,它会在不提出疑问的情况下运行这个程序,并且当问题行被运行时,它并不会显示出错误。在这个例子中,问题可以很快被发现,但是想象一下同样的问题出现在成百上千的代码行中间、在极其复杂的函数中间……
总而言之,寻找错误的源头是个噩梦!
在OCaml中,如果你想尝试做这种类型的操作,你会在代码运行之前立马得到一个类型错误,这样就可以提前避免大量的错误!
但是为了确认这些类型,OCaml需要知道每个表达式的类型。
在这项任务中,正确使用操作符会很有帮助:加法+
适用于int
类型,加法+.
适用于float
类型。
这就是为什么我们不能在各个地方使用同样的操作符的原因!
3.更多的类型!
正如我们在上一节中看到的内容,OCaml有一个整数int
类型,以及一个浮点数float
类型,和Python中一样!唯一的区别是对于不同的类型会有不同的操作符。
int |
float |
|
---|---|---|
例子 | 2 , -2 |
2. , 1e3 , 3.14 |
加法 | + |
+. |
减法 | - |
-. |
乘法 | * |
*. |
除法 | / |
/. |
求余数 | mod |
mod_float |
当然OCaml还有其它基本类型。
布尔(Boolean)
bool
类型同样存在于OCaml中,并且运行方式也与在Python中类似。最大的区别在于,我们在书写true
和false
时不再大写。尽管逻辑操作符的书写方式并不一样,但是运作方式是一样的。
逻辑与 | && |
逻辑或 | || |
逻辑非 | not |
比较操作符和在Python中几乎是一样的:<
、<=
、>
、>=
。
只有等于号与不等于号有所改变:在OCaml中,我们将分别使用=
和<>
(==
和!=
也同样存在,但并不是期望中的意义)。比较操作符的优点是,不管何种类型,只要表达式左右两侧是同一类型就可以(我们可以比较两个int
或者两个float
,但是不可以比较一个int
和一个float
)。
文本
在OCaml中,需要区分单个字符与字符串。
- 第一,
char
类型写在单引号之间:’a’。 - 第二,
string
类型写在双引号之间:”Bonjour !”。
注意,'a'
和"a"
是不一样的。另外,与支持ASCII表与Unicode(支持带音调字母、其它字母表中字符、表情符号等等)的Python不同,OCaml几乎不支持Unicode:所以我们在使用时最好避免在文本中输入带音调字母。
另外,如同+
适用于两个整数之间,连接两个string
类型所使用的的操作符为^
。
从一个类型到另一个类型
OCaml提供了好几个函数来切换不同的类型。 这些函数的名字都有一个固定的格式DEST_of_ORIG
,DEST
是目标类型,ORIG
则是原始类型。比如说,使float
切换到int
,我们会使用int_of_float
。
你现在还不知道如何调用函数,但这是我们下一节的目标!
4.函数
正如我们之前提到过的,函数是OCaml语言的核心。接下来我们将学习如何创建以及调用函数。
创建函数
在OCaml中,我们使用关键词let
来定义一个函数:
let f x y = x + y
复制代码
为了更好理解代码行的含义,以下是在Python中的相同程序:
def f(x, y):
return x + y
复制代码
在此我们标注几点不同:
- 参数并不写在括号中,而是直接写到函数名后面;
- 参数之间也不用逗号隔开,用空格分隔即可;
- 函数的主体从符号
=
开始; - 函数主体只包含一个表达式,而不是一系列指令(instruction)。
- 唯一的指令(instruction)永远是函数返回的结果。
这种极简的句法在最初会看起来有些陌生,但我们会很快习惯它,并且当我们日后深入使用OCaml时会发现它很方便(参照附加部分)。
如果在OCaml解释器中输入以下命令,我们将会得到函数的类型:
f
复制代码
我们可以看到这个函数的类型为:int -> int -> int
。关于这个注释我们需要知道,前两个类型为参数的类型,而最后一个则是返回的值的类型。这里,参数x
和y
为int
,它们的和也为int
。如果你想了解为什么选择了这样不太直观的注释,我们将会在柯里化这一节中讨论。
一个实用的做法(在学校的考试中也是这样要求的)是明确批注出函数的类型。针对这一点,我们使用句法(参数:类型)
来代替参数
。对于返回的值,我们在=
前使用:type
来指出其类型。
let f (x : int) (y : int) : int = x + y
复制代码
这个注释会更长一些,但它的优点是可以明确地显示出不同表达式的类型。同样,这也使得我们更容易理解这个函数,因为我们可以很清楚地看到参数以及返回的值可以取什么样的值。
小窍门:乘法的定义
我们也可以一次同时定义几个函数,只要用and
(注意:与&&无关)来分开它们的声明即可。
let identite x = x and carre x = x * x and cube x = x * x * x
复制代码
但是在此句法中,我们不能使用一行中靠前的函数来定义后面的函数。 比如,我们不能写成这样:
let carre x = x * x and cube x = (carre x) * x
复制代码
因为当我们定义cube
时,函数carre
并不存在在环境中(还没有被运行)。
调用函数
现在我们拥有了第一个函数(不要犹豫去创建其它函数),来看一下我们是如何调用它的。
f 1 2
复制代码
就这样!通常来讲,解释器将会回复3
。
如果你需要使用更复杂的参数,那么你可以用括号括起来:
f (4 * 3) 7
复制代码
以及变量?
事实上,变量的概念并不存在于OCaml中refs。但是,我们可以有没有参数的函数常量。
let pi = 3.1415
复制代码
此外,我们还可以使用表达式来限制函数,那么就可以仅仅使用本地方式来定义一个函数。对此,在声明完函数之后,我们加入关键词in
以及一个表达式,这样我们就可以使用刚刚定义好的函数。
let cube x = x * x * x in (cube 2) + (cube 3)
(*在不定义cube的情况下,我们可以这样写:*)
(2 * 2 * 2) + (3 * 3 * 3)
复制代码
通常我们会在in
之后换行来使得代码更易于阅读。同理我们也可以嵌套let
和let/in
:
(*这个函数计算了平面中的范数,已知坐标*)
let norme (X : float) (y : float) : float =
let carre a = a * a in
let x_carre = carre x in
let y_carre = carre y in
sqrt (x_carre + y_carre) (*sqrt是OCaml中的基础函数*)
复制代码
5.课后练习
为了验证你是否已经理解了到目前为止的内容,你可以尝试回答下列问题。如果答对的话,你的回答会从红色变为绿色。
(因为编者是在网页上撰写的,但是掘金上并不支持这样的代码格式,所以译者稍加改动。可以打开开头的源网页来测试。)
下列表达式的类型分别是什么?如果其类型不成立,则输入错误
。
2 + 2
复制代码
<答案点我>
int
复制代码
2e3 +. 2.5 *. (float_of_int 4)
复制代码
<点我>
float
复制代码
'H'
复制代码
<点我>
char
复制代码
12 > 3 && 7
复制代码
<点我>
错误
复制代码
"你" ^ "好" ^ "!"
复制代码
<点我>
string
复制代码
42 > 12 || 19 > 38
复制代码
<点我>
bool
复制代码
let ajouter x y = x + y
ajouter (*我们询问的是ajouter的类型*)
复制代码
<点我>
int -> int -> int
复制代码
'A' + 3
复制代码
<点我>
错误
复制代码
(*我们假设'x'和'y'是之前定义过的常量
*但是我们并没有为其赋值
*并且:这个表达式并不是一个错误类型
*)
x +. (y *. y)
复制代码
<点我>
float
复制代码
下列的布尔类型表达式是真(true
)或假(false
)?
42 > 12 || 19 > 38
复制代码
<点我>
true
复制代码
4 * 5 + 2 = 28
复制代码
<点我>
false
复制代码
'a' < 'e'
复制代码
<点我>
true
复制代码
let x = 15
x > 2 && x < 17
复制代码
<点我>
true
复制代码
"Zoé" > "Alice"
复制代码
<点我>
true
复制代码
___
中应该填写什么,使得rusultat
的值为38
?
let ____ = (2 * x * x) + (3 * x) + 11
let resultat = polynome 3
复制代码
<点我>
(polynome x) 或 (polynome (x : int))
复制代码
如果你还想要尝试更多的练习,下列是一个真正的习题。
实现下列函数:
- 计算一个浮点数`x`的立方;
- 一个有三个参数的函数,其参数分别为`a`、`b`和`ε`,并使其分辩在给定的` ε`条件下,`a`是否等于`b`。为了简化这项任务,我们默认`a`总是大于`b`;
- 该函数的参数为一个字符与一个整数,并确认在ASCII表中,该字符对应的编码是否与这个整数一致。你可以先测试以下字符`A`,其在ASCII表中对应编码为`65`。
复制代码
<点我>
let cube (x : float) : float = x * x * x
let egal (a : float) (b : float) (epsilon : float) : bool =
let diff = a -. b in
diff < epsilon
let est_ascii (c : char) (i : int) : bool =
(int_of_char c) = i
复制代码
当你完成之后,你就可以进入下一章进行学习了。
如果你有很多问题的话,毫无疑问,最好的解决办法是重新阅读一遍第一章的内容,并且自己测试一下所有例子中的代码,然后自己再举一些另外的例子。
如果你觉得有些地方讲的不是很清楚,需要解释的话可以给这个邮箱地址发邮件。
ocaml @ gelez . xyz
- 事实上,这和’引用(reference)’的概念是类似的,但是我们会避免使用它,并且在本教程中也不会涉及。↩