【译】【OCaml函数式编程教程】第一章:基础概念

译者前言

译者于一年前首次于课上接触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中类似。最大的区别在于,我们在书写truefalse时不再大写。尽管逻辑操作符的书写方式并不一样,但是运作方式是一样的。

逻辑与 &&
逻辑或 ||
逻辑非 not

比较操作符和在Python中几乎是一样的:<<=>>=
只有等于号与不等于号有所改变:在OCaml中,我们将分别使用=<>==!=也同样存在,但并不是期望中的意义)。比较操作符的优点是,不管何种类型,只要表达式左右两侧是同一类型就可以(我们可以比较两个int或者两个float,但是不可以比较一个int和一个float)。

文本

在OCaml中,需要区分单个字符与字符串。

  • 第一,char类型写在单引号之间:’a’。
  • 第二,string类型写在双引号之间:”Bonjour !”。

注意,'a'"a"是不一样的。另外,与支持ASCII表与Unicode(支持带音调字母、其它字母表中字符、表情符号等等)的Python不同,OCaml几乎不支持Unicode:所以我们在使用时最好避免在文本中输入带音调字母。

另外,如同+适用于两个整数之间,连接两个string类型所使用的的操作符为^

从一个类型到另一个类型

OCaml提供了好几个函数来切换不同的类型。 这些函数的名字都有一个固定的格式DEST_of_ORIGDEST是目标类型,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。关于这个注释我们需要知道,前两个类型为参数的类型,而最后一个则是返回的值的类型。这里,参数xyint,它们的和也为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之后换行来使得代码更易于阅读。同理我们也可以嵌套letlet/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


  1. 事实上,这和’引用(reference)’的概念是类似的,但是我们会避免使用它,并且在本教程中也不会涉及。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享