OCaml语言编程基础教程(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-29 08:10:40

点击下载

作者:陈钢 张静

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

OCaml语言编程基础教程

OCaml语言编程基础教程试读:

前言

2016年6月14日,华为宣布在法国设立数学研发中心。华为战略与市场总裁徐文伟在揭幕仪式上指出,法国“菲尔茨奖得主多达12位,仅次于美国。数学的研究正在为ICT产业带来全新的突破。”实际情况正是如此。法国在计算机科学方面的一项在国际上有重要影响的成果是OCaml语言,这一语言的诞生与发展得益于法国雄厚的数学基础。

法国菲尔兹奖获得者大都来自巴黎高等师范学院,或与其有关。这个学校占地面积不如中国一所小学,但它却是法国数理化等基础科学的研究中心。20世纪八九十年代,巴黎高等师范学院秉承其数学传统,在范畴论的基础上研发了 λ 演算的一个语义模型,称为“范畴抽象机”(Categorical Abstract Machine),此后又在这一模型的基础上研发了函数式语言ML的一个新的变种,称为Caml。之后,法国人又在Caml语言的基础上增添了面向对象的机制,形成了OCaml语言。后来,OCaml的研发中心转移到INRIA(法国国家信息科学研究中心)。

OCaml语言的诞生和发展都同数学基础密切相关。OCaml语言出现之后,所进行的一项最著名的工作,就是开发Coq定理证明器,它是一个基于高阶带类型 λ 演算的交互式定理证明工具。Coq的主要用途在于两方面,一方面是用于形式化数学(Formalized Mathematics)的研究工作,就是把数学知识用形式化方式表示,并检查数学证明本身的可靠性;另一方面是进行程序的形式化验证。

OCaml语言是一种函数式程序设计语言。函数式语言是一群追求数学美的学者所研发的语言。他们对完美性的重视甚于对实用性的重视,这并不等于说他们不看重实用性,只是他们把完美性放在优先的位置。法国是一个特别注重理想主义的国家,这种理想主义品格也深深地注入到了OCaml语言当中。为了达到数学上的完美性,就需要有更多的付出,同时也会带来更好的长期回报。

函数式语言的发展经历了一个曲折的过程。其间走过很多弯路,遇到很多障碍,也有很多成功的惊喜。如果你喜欢冒险和挑战,愿意接受前进路上的不确定性,对失败和挫折有承受能力,乐于克服困难并享受解决问题之后的喜悦,你应该选择函数式语言。

函数式语言的历史同计算机语言的历史一样长。最早的函数式语言是LISP,它出现在C语言之前。当然,它远不如C语言等命令式语言那样普及。在计算机的发展历史中,人们不断地掀起对函数式语言的热情,但是,在相当长的一段时间内,能够在这个领域中坚持下来的人为数不多,不过,这种情形近年来正在发生改变。

如果想更具体地了解函数式语言的特点,最好的方式是看一个典型的函数式语言程序,并把它同C语言程序做比较。要讲解一个有意义的程序例子,需要先讲解一定的基础知识,这些内容超出了前言的范围。急性子的读者可以翻阅一下2.5.4节中的排序函数的例子。它是一个很能说明函数式语言优缺点的案例。我们把用OCaml实现的排序函数同C语言实现的排序函数做了比较,并且归纳了函数式语言的3个优点:

第一,灵活性和通用性。C排序算法仅仅针对一种特定数据类型的元素进行排序,例如,对整数数组排序。但OCaml写出的排序算法是通用的,不但可以对整数序列排序,而且可以对实数序列、字符串序列,以及各种结构化元素构成的序列进行排序。OCaml能够做到这一点是因为语言中包含了多态类型和高阶函数等成分,这些概念都是C语言没有的。

第二,安全性。C语言编写的排序程序中包含了大量的数组元素访问操作,这类操作很容易出现数组越界错误,从而引起程序崩溃。OCaml的排序程序使用了表数据结构,表操作不会发生类似数组越界这样的严重错误。因此,OCaml的排序程序代码比C排序代码安全。

第三,简洁性。排序的核心操作是一个partition函数。C代码写的partition函数可读性差,而OCaml所写的partition函数简洁易懂,同算法逻辑的吻合很好,代码行数不足C函数的三分之一。

具有上述优点的函数式语言20多年前已经存在。今天的函数式语言具备更丰富的特性,例如,模块化程序设计能力、复杂的类型推导系统等。然而,函数式语言的普及和推广至今还没有完成。这是因为,影响一个语言的推广应用有很多因素,例如,是否有功能丰富的函数库,是否有足够多的成功案例,是否易学易懂,等等。

一个语言的典型应用对语言的普及有很大的影响。C语言被成功用于开发UNIX操作系统,随后,围绕UNIX进行的系统程序开发和应用程序开发都在C语言之上进行。OCaml语言的首个重要应用是Coq定理证明器的开发。同操作系统相比,定理证明器的用户要少得多,而且这些用户并不一定需要用OCaml编程。所以,OCaml语言的推广发展远不如C语言那么快。在很长一段时间内,OCaml的主要用户是一些大学和研究单位,它被用于开发新的定理证明器及其他研究性项目。

同C语言相比,OCaml的发展比较缓慢,但是它一直在稳步前进,用户群也在不断扩大。在现有的函数式语言当中,OCaml是用户最多的语言之一。在应用软件开发中,OCaml的知名度也在不断提高。

OCaml是否容易学习?这个问题因人而异。网上有一位清华学生说,他(她)跟法国OCaml专家学了一段时间,发现学OCaml比零基础学C语言难多了。但是也有同学说,他们一周时间就学会了OCaml。一般而言,一些数学基础好的人比较容易适应OCaml。而对C语言经验丰富的程序员已经养成了一种编程习惯,他们可能反而觉得OCaml难学。因此,学好OCaml要注重培养新的编程习惯,这也是本书写作的重点。

OCaml语言是 λ 演算理论(包括无类型 λ 演算和带类型 λ 演算)的一次出色的应用,同时也是学习λ演算以及基于 λ 演算的众多理论和技术的入门课程。建议有条件的读者把OCaml语言同 λ 演算两门课程结合在一起学习。本书也讲解了一些 λ 演算的基本概念。

事实上,在法国,OCaml语言是学习一系列后续课程的基础课程。这些后续课程包括 λ 演算、类型理论、重写系统、形式语义、程序语言实现方法、高阶定理证明器、进程演算、同步语言、程序分析等。

在互联网时代,能否防范黑客攻击成了软件质量的一个重要指标。在这方面,OCaml语言的优势进一步显现出来。黑客攻击的主要方法是利用软件本身的缺陷入侵到软件内部,而OCaml语言编写的程序可以避免很多C程序中的缺陷,例如缓存溢出和存储泄漏,因此也能够更好地抵抗黑客的攻击。

近20年来各种新型语言。例如Javascript、Go、Scala、Groovy、Elixir、Clojure、Ruby、F#等层出不穷。几乎所有的新语言都深受函数式语言的影响,一些新语言本身就是函数式语言。这些新语言在一定程度上是面向应用的函数式语言,它们在函数式程序设计概念的基础上添加了针对各种专门应用的语言成分。OCaml语言出现在这些新语言之前,它的主要历史功绩是推动了各种函数式程序概念和技术的发展,尤其是语言中的类型系统的发展。它是函数式语言领域做出重要创新的先驱之一,很多新语言中的函数式概念都来自OCaml以及其他一些专业性的函数式语言。因此,对于系统性地学习函数式编程技术,认真学习OCaml语言是很有帮助的。

F#是微软公司开发的OCaml语言变体。它在OCaml基础上添加了大量的具有实用价值的功能,使用户能够利用Windows平台中丰富的库函数,为普通用户提供了很多方便。但是,从语言特性角度看,F#同OCaml依然有差距,例如在模块和函子方面,F#的能力远不如OCaml强大。因此,如果想在函数式程序设计方面得到充分的训练,F#不能替代OCaml。尽管如此,考虑到F#对应用开发的重要性,本书中拿出了一定的篇幅讲OCaml程序怎样移植到F#。

在动手写这本书的时候,国内OCaml教材只有一本《Real World OCaml》【4】的译著,该书对有经验的OCaml程序员很有帮助,但不太适合初学者,在基础训练方面做得不够。在英文教材中,可以考虑的教材有【1,2,3,4,5】。【1,2】是经典的OCaml著作,有权威性,但写作时间比较早,有些程序在最新的OCaml解释器上不能运行,OCaml的一些新发展也未能包含进去;【3】主要适合有经验的OCaml程序员;【5】是专门给初学者写的书,没有包括诸如函子(functor)和面向对象等重要内容。其他一些教材主要讲OCaml语言在某个专业领域的应用,因此并不很适合教学。因此,我们想写一本能够包括OCaml的主要特征,同时能够提供函数式编程基本训练的教材。

教材【1】为函数式语言基本技能训练提供了很好的材料。征得原书作者Guy Cousineau和Micheal Mauny的同意,本书第1章和第2章部分内容翻译自教材【1】。在此对原书的作者表示深切的谢意。同时,我们也增补了很多近年来的新内容。针对国内初学者的特点,本书对函数式编程各方面的概念做了进一步的解释,并增加了OCaml语言同命令式语言的对比。

本书的重点在于讲解函数式编程的基础知识以及OCaml程序设计的技巧,同时兼顾应用软件开发的需求。在写作过程中,很多地方我们把OCaml编程方式同其他语言的编程方式进行比较,便于熟悉其他语言的程序员理解OCaml的特点。我们将看到,同C语言相比,OCaml程序更加精巧,便于进行程序分析;同无类型的函数式语言LISP相比,OCaml增加了类型推导机制,提高了程序的安全性。

本书第1~5章及第8章全部是关于OCaml本身的内容。由于OCaml在用户界面开发等方面不够理想,影响了OCaml的普及。因此第6章和第7章讲了如何通过引入F#和C#来补充OCaml在这方面的不足。F#是微软在OCaml语言基础上开发的一种语言,它同OCaml部分兼容,同时又能利用微软的大量库函数。第6章讲了如何把OCaml移植到F#。第7章讲了怎样通过C#开发的用户界面调用OCaml或F#程序,这个内容也是多语言联合软件开发的一个案例。现代实用软件的开发往往需要结合多种语言的优势。第8章讲了面向对象程序设计。

本书一方面介绍了OCaml程序设计的基本概念以及函数式编程的基本方法,同时尽作者所能介绍了OCaml语言的最新发展,此外还提供了一组规模适度的软件开发案例。3.7节提供了一个质数生成模块案例;4.10节包含了一个四方向链表案例;第5章结合基于模块的开发方法介绍了一个电机作图的案例;第6章结合对F#的介绍讲解了基于F#的电机作图的案例;第7章结合多语言混合设计继续用电机作图的图形化用户界面的设计;8.15节用面向对象方式实现电机作图。

由于作者水平有限,书中错误在所难免,欢迎读者及时指正。陈钢航天科工集团北京京航计算通讯研究所2018年1月致谢

感谢我的博士导师Guissepe Longo和Guissepe Castagna。他们的帮助和指导使我不但获得了在法国学习的宝贵机会,而且能够进入程序语言设计这一激动人心的领域。他们在学业上给了我很大的帮助,尤其是在范畴理论和面向对象的程序语言的类型理论方面,使我学到很多重要的理论,他们的帮助使我能够顺利完成博士学位。感谢给我讲授OCaml语言的老师:Guy Cousineau和Micheal Mauny。本书前两章部分内容翻译自他们所写的Caml语言教材。他们是Caml语言的主要发明人,Caml语言是OCaml的前身。感谢给我们讲授λ演算的老师Thérèse Hardin,λ演算是函数式语言的基础;感谢OCaml语言类型理论的老师Xavier Leroy和Didier Rémy,他们也是OCaml语言的主要开发人员;感谢讲授形式化语义的Roberto Di Cosmo老师;感谢讲述构造演算和高阶类型理论的老师Gilles Dowek,构造演算是COQ定理证明器的基础理论,COQ也是用OCaml开发的一个重要的软件。

本书是我在北京京航计算通讯研究所工作期间完成的。感谢集团高红卫等领导对我的工作的鼓励和支持。感谢老所长李艳志和所领导王俊、于林宇、于会、刘军、张津荣、郑德利对我的工作的支持和帮助;感谢舒毅、张静、占银玉,他们在担任我的助手期间做了很多重要的工作。张静在实习期间开始翻译Guy Cousineau和Micheal Mauny的OCaml教材,并且和我一起开始着手这本书的撰写,实习结束后继续花费大量的时间进行本书的撰写和修改工作,非常认真仔细,并且从读者角度提出许多有益的看法,在排版和美化方面做了主要的工作。感谢所内各部门领导李昆、朱琳、王颖、刘伟、王家安、韩旭东、李娜、宋文、魏伟波、魏鑫在我的工作和生活上多方面的帮助;感谢所内同事孟伟、吕宗辉、郑金燕、于润泽、张志刚、王栋、杨楠、张国宇、张明敏、李卓、李丽华、刁立峰、彭鸣、姚可成、高飞、赵静、王佳佳、黄云、宋悦、刘玉峰、李思等在日常工作中的协助。感谢李芳、焦留、杨杰、房静在后勤方面的支持。

感谢裘宗燕教授为本书撰写序言并且提出宝贵意见。感谢孙家广、宋晓宇、蒋颖、顾明、王戟、杨志斌教授,在同他们的合作中,我进一步了解了国内对OCaml语言的需求。

感谢我的妻子胡萍,长期以来她一如既往地支持我的研究工作,在生活和精神上都给了我很大的支持。陈钢航天科工集团北京京航计算通讯研究所2018年1月16日第1章函数式控制结构

OCaml支持函数式、命令式以及面向对象程序设计。但是它的主要特点是支持函数式程序设计。

在函数式程序设计提出了“纯函数”的概念指的是只做输入输出变换,没有副作用的函数。为了说明这一概念,我们先来看两个C函数的例子,第一个:int f (int a) { return (a+1); }

这个函数的输入值是一个整数a,输出值是a+1。这个函数实现了输入a到输出a+1的变换,除此之外没有其他功能。所以,这个函数是“函数式程序设计”意义上的函数。再看第二个例子:int g (int a) { b = a; return (a+1); }

这个函数同样把输入的整数a加一之后输出。但是,它还对一个全局变量b进行了赋值,这个操作就称为函数的副作用。因此,g就不是纯函数式程序设计意义下的函数。

函数式程序设计风格的一个重要优点是提高了程序的可靠性和可理解性。它的一个缺点是有时会降低程序的执行效率。

函数式语言的基本控制结构主要有函数定义和函数调用。

核心OCaml程序主要由let定义(definition)和表达式(expression)组成。和C语言相比,OCaml的核心部分严格地说没有“语句”概念。OCaml表达式既起到C语言中表达式的作用,又起到语句的作用。OCaml中的定义类似于C语言中变量声明和函数定义的结合体,它本质上是把一个名字和一个表达式相关联。

表达式的特点就是能够通过计算得到一个值(value)。值是无法继续计算的表达式,函数也是一种值。值都有类型,因此表达式都有类型。OCaml的let定义把变量声明、变量初始化和函数定义等概念整合在一起。本章主要讲OCaml中的表达式以及基本的let定义。表达式和let定义是OCaml解释器能够执行的基本单元。1.1 OCaml解释器

C语言是基于编译器的语言,而OCaml是基于解释器的语言。对于一个基于编译器的语言,必须写出一个完整的程序才能编译执行。而对于一个基于解释器的语言,程序可以划分为一组可独立执行的代码,解释器可对这组代码顺序执行。基本的OCaml程序由一组定义和一组表达式构成,这些定义和表达式可以在OCaml解释器中按顺序单独执行。OCaml程序也可以编译执行,实用的OCaml应用开发都需要编译。在学习OCaml的时候,我们首先从解释执行开始。程序的解释执行使得语言的学习更为方便。在OCaml解释器中执行OCaml定义和表达式的过程称为交互式会话(interactive session)。

OCaml是一个免费的软件,可以从ocaml.org上下载。在安装了OCaml软件的Linux或Cygwin中,可以通过OCaml命令启动OCaml解释器。在Windows平台上,过去有一个OCamlWin窗口程序,可以在窗口界面中运行OCaml解释器。近几年OCaml的发布方式经常有变化,带窗口界面的OCaml软件不太容易找到。OCaml官方下载页面上现在可以看到三种Window平台下的OCaml安装方案,推荐使用最后一个,即下载OCaml64.exe程序,该程序执行之后会创建一个目录,缺省情况下的目录名是C:\OCaml64,其中包含一个cygwin工作环境。通过在桌面上新创建的OCaml64图标可以启动这个工作环境,它是一个类似linux终端的命令行操作界面。在这个界面中可以执行linux命令,并且可以执行一个opam程序,它是一个用于安装OCaml及相关软件的专用工具。执行opam init将对这个工具做初始化,同时下载并安装最新的OCaml软件。安装完毕之后,可以键入ocaml命令启动OCaml解释系统,如图1-1所示:图1-1

解释执行OCaml比较好的方式是在XEmacs中安装Tuareg模式,这样不仅可以得到一个支持OCaml编辑的XEmacs模式,而且可以在编辑器中直接运行OCaml解释器。如果没有在本地机器中安装OCaml软件,也可以在网上直接使用在线的OCaml解释器。网站http://try.ocamlpro.com/上不仅提供了可执行OCaml的在线解释器Try OCaml,而且还提供了一些教学材料。

进入OCaml解释器之后就可以进行交互式会话。解释器会显示一个提示符“#”,等待用户输入。此时用户可以输入一个OCaml表达式,用“;;”结尾,按键输入到系统中。如果表达式语法正确,解释器会做出一个回应(response),它包括表达式的计算结果,以及对表达式的类型分析结果,前者称为表达式的“值”(value),后者称为表达式的类型(type)。下面是交互式会话的一个例子:# "Hello World!" ;;- : string = "Hello World!"

很多语言的教学都从一个Hello World程序开始,这样的程序代表了一个语言中有代表性的最简单的程序。对于OCaml语言,最简单的程序就是"Hello World!"字符串。

在这个例子中,系统的回应分为两部分,第一部分是“- : string”,它表示用户输入表达式的类型是字符串。第二部分是“= "Hello World!"”,它表示输入表达式的计算结果是等号右边的"Hello World!"。

在这个简单的例子中,表达式的值(表达式的计算结果)就是表达式本身,即一个字符串。值得注意的是,系统自动分析出了表达式的类型“string”。

OCaml的类型分析被称为“类型推导”(type inference)或“类型合成”(type synthesis)。它是指通过对表达式的分析自动推导出表达式的类型。这一分析工作是在计算表达式之前做的,因此OCaml的类型系统是静态类型(statically typing)系统。OCaml是强类型(strongly typing)语言,它对代码进行严格的类型检查,保证了程序中不会出现类型错误。在其他语言中,需要对变量和函数进行显式的类型说明,而在OCaml语言中,变量和函数的定义可以不包含类型说明,系统自动推导出它们的类型。类型推导是OCaml语言的重要特色,对此本书会进行重点讲解。1.2 表达式和let定义

OCaml语言入门很容易,可以把它当作计算器,进行算术表达式的计算。# 2 + 3 * 5 ;;- : int = 17

在这个回应中,符号“-”表示这个表达式是由用户输入的,“int”是表达式的类型,它说明这是一个具有整型值的表达式,“17”是表达式的值。

如果想把表达式的计算结果保存起来,并在后续计算中使用,可以通过let结构把表达式的计算结果保存在一个变量中,例如:# let a = 123 * 456 ;;val a : int = 56088

这个let结构也称为let定义,或简称定义。它的语法格式是:let <变量> = <表达式>

其中,变量是一个由小写字母或下划线开头的,由大小写字母、数字和下划线构成的标识符。

对于let定义,系统的回应以“val”开始,表示标识符a是一个变量,用来保存一个值。上面的回应说明,a的类型是int,即整数类型,a的值是56088。

在这里,我们再一次看到了OCaml语言的类型推导能力。在let声明中,并没有说明a的类型,只说明了a将保存表达式123 * 456的计算结果。系统对表达式进行类型分析,推导出这个表达式的类型是整型,从而确定a的类型为整型。

在建立a的定义之后,后续的表达式和定义就可以引用这个变量。例如:# a + 4 ;;- a : int = 56092# let b = a + 5 ;;val b : int = 56089

在计算一个表达式的时候,表达式内的变量必须在之前已经定义过,不然会产生变量无定义的错误。例如:# x + 4 ;;Characters 0-1: x + 4 ;; ^Error: Unbound value x

let定义起到了其他语言中变量声明的作用,只是它不需要进行变量的类型声明,但是必须给变量一个“初始值”。不允许只声明变量而不给“初始值”。这样做的一个好处就是避免了忘记给变量赋初值的问题。

但是,这里的变量定义和命令式语言中的变量声明是不同的。例如,C语言中,在一个函数体内变量不能重复声明。下面的程序会导致编译错误:void main () { int i = 1; int i = 2; }

但是在OCaml语言中,一个变量可以合法地重复定义:# let i = 1 ;;val i : int = 1# let i = 2 ;;val i : int = 2

在这里,let定义看上去和赋值语句相似,但实际上它和赋值过程不一样。在使用赋值语句时,不能对同一个变量赋予不同类型的值。但是,在同一段程序中,可以使用多个let定义,把一个变量和不同类型的值相关联。例如:# let i = 1.2 ;;val i : float = 1.2# let i = "abc" ;;val i : string = "abc"# i ;;- : string = "abc"

也就是说,每做一次定义,就会把之前的定义覆盖掉。因此,let既不是变量声明,也不是变量赋值,而是动态地建立了变量和值的一个关联(variable associated with a value)。我们可以把这个关联看成是变量与值的一个对偶:(变量,值)。程序中的一组let定义建立了变量与值的一个对偶序列,可以写成:

这样一个序列称为“环境”(environment)。在表达式求值时,OCaml到当前的环境中去寻找表达式中变量所关联的值。

从实现角度看,命令式语言中的变量声明是给变量分配一个固定的空间,变量赋值则是对这个空间中内容的修改。而let定义则是给变量动态地分配一个空间。

命令式语言中的一个常见的程序错误就是赋值语句两边的类型不一致。例如,在C语言中可以写出这样的程序:int main() { int i = 1; float a = 1.2; i = a; // 浮点数赋值给整数}

由于编译器为不同类型的变量分配的存储空间大小不同,因此数据在不同类型的变量之间传递会造成信息丢失或变形,从而产生错误。在实际应用中,这种错误可能会引起灾难性的后果。1996年6月,欧洲阿丽亚娜5号在发射升空40秒之后爆炸,就是因为浮点数向整数转换时产生了错误。

在OCaml中可以写出同上面的C语言代码表面上相似的程序:let i = 1 ;;let a = 1.2 ;;let i = a ;;

OCaml编译器不会对它报错,同时也不会发生类型转换错误。这是因为程序中的两个i并不占据相同的存储空间,它们可以看成是占据不同存储空间的两个不同的变量,而且具有不同的类型。只是在后一个i定义之后,前一个i自动失效。

OCaml语言的存储自动分配机制和类型推导机制使它成为一个远比C语言更安全的语言。

纯函数式语言的一个标志性特征是没有赋值语句。上面的分析显示,let定义在某些情况下可以替代赋值,但本质上却不同于赋值。

前面所讲的let定义都是顺序执行的,之前定义的变量可以在后面的定义中使用。let定义还有一种并行定义结构,它对一组变量同时定义:let v1 = e1 and v2 = e2 ... and vn = en

如果一个变量在并行定义的表达式中出现,那么它所取的值是整个并行定义之前给这个变量定义的值,不是并行定义中对这个表达式所定义的值。如果这个变量在并行定义之前没有定义过,那么会出现变量无定义的错误。另外,在并行定义中变量定义的先后顺序不会影响定义结果,所有变量同时被定义。例如:# let a = 1 ;;val a : int = 1# let a = 2 (*第一个并行定义*) and b = a ;; (* The same as: let b = a and let a = 2 ;; *)val a : int = 2val b : int = 1# let x = 1 (*第二个并行定义*)and y = x ;; Characters 18-19: and y = x ;; ^Error: Unbound value x

在第一个并行定义的let and结构中,a和b同时被定义,b的值定义为a,此时a的值为1,同时a的值定义为2。正因为a和b是在同一时间被定义,所以b只能取到a在并行定义之前的值,不可能取到并行定义中a的值。在第二个并行定义中,x虽然在并行定义中有定义,但是在并行定义之前并没有定义,因此“let y=x”出错。

注意,OCaml中的注释由“(*”开始,以“*)”结束。

let定义是全局性的。在一个变量被重新定义之前,变量的定义一直有效。下面一节描述一种建立局部定义的方法。1.3 let局部定义

let定义有一个扩展形式,我们把它称为let局部定义,简称局部定义,其语法格式如下:let <变量> = <表达式1> in <表达式2>

在这个扩展形式中,变量不再是全局有效的变量,它的作用域局限于<表达式2>。在下面的例子中,首先定义了一个全局有效的变量x,它的值是3;然后在局部定义中,把x的值关联到1,计算x + x的结果是2;在完成这个局部定义的计算之后,x的值又恢复到原来的值3。# let x = 3 ;;val x : int = 3# let x = 1 in x + x ;;- : int = 2# x ;;- : int = 3

局部定义是一种表达式,而不是定义。系统对表达式的回应以“-”开始,而对定义的回应以“val”开始。从语义角度看,let局部定义等价于<表达式2>中把变量全部替换成<表达式1>的结果,即有下述语义等价关系:

这里表示把表达式中的变量全部替换成表达式的结果。例如:

表达式的主要特点就是有一个类型和一个输出值,的类型和输出值就是的类型和输出值。

相比之下,let定义的语义是把一个变量和一个值关联在一起,并且扩展了环境。所以,虽然let定义和局部定义都是由let开始,但却具有不同的语义作用。

此外,正因为局部定义是表达式,所以它可以嵌套使用。例如:# let x = 1 in let y = x + x in x + y ;;- : int = 3

目前,我们已经看到了两种类型的表达式:一种是算术表达式,另一种是局部定义表达式。在OCaml中,表达式是一个很强大的概念。后面我们将看到,控制语句和函数等复杂结构都是表达式。

一个由OCaml核心语言所写的程序可以由一组let定义和一个表达式构成。也就是说,核心OCaml程序的架构具有下述形态:let f1 = e1 ;; let f2 = e2 ;; ... let fn = en ;; e ;;

其中的let定义相当于C语言中全局变量定义和函数定义,最后的e相当于主程序。实际上,一个完整的程序可以不用let定义。例如,上面的程序架构可以直接用let局部定义重写如下:let f1 = e1 in let f2 = e2 in ... let fn = en in e ;;

也就是说,一个let局部定义就可以构成一个完整的程序。但是,let定义可以让我们在解释器中以交互式的方式编程,每个let定义能够独立让OCaml接受并执行。另一方面,let定义有助于程序的模块化。一个比较大的应用程序通常由一组程序文件组成,每个文件可以由一组let定义构成,这样的文件可以单独编译。

注意let定义只能用于全局定义,不能在表达式内部使用。因此,在函数和“控制结构”中都不能使用let定义。这也是赋值语句和let定义之间的一个重要区别。在表达式内部只能使用局部定义,它可以在某种程度上实现赋值语句的功能。

虽然let定义(包括局部定义)和赋值语句有很强的相似性,但不能完全取代赋值语句的功能。例如,在命令式语言中,我们通常会用赋值语句去更新一个循环变量。但在纯函数式语言中,无法直接使用这一编程模式。函数式语言有独特的处理循环的方法,学习函数式语言编程需要掌握新的编程模式。

OCaml语言实际上在纯函数式语言的基础上加入了命令式语言的成分,其中也包括命令式的赋值语句。但在OCaml语言的初学阶段,要避免使用命令式编程,首先学好函数式编程技巧。

和let定义相似,局部定义也有并行结构,例如:# let a = 3 in let a = 2 and b = a in (* 也可以写成: let b = a and a = 2 *) a - b ;; (* a = 2 , b = 3 *)- : int = -1

下面先介绍基本的数据类型,然后讲解函数定义。函数的递归定义可以让我们实现循环程序所要达到的功能。1.4 基本类型

在OCaml语言中,类型是一个非常重要的概念。从语义上看,类型可以直观地理解为一个集合,它包含一组元素。在OCaml语言中,这些元素称为值(value)。“值”是计算过程中所用到的数据以及计算的结果。类型是对值的分类。表达式的类型是表达式所计算出的值的类型。

为了叙述方便,有时我们也称类型中的元素为“对象”。我们会使用术语“对象的类型”以及“对象具有某个类型”。这里所说的“对象”不是面向对象语言中的对象。

程序语言中的类型可以分成两类,一类是系统预定义的基本类型,另一类是在基本类型的基础上构造出的类型。和C语言类似,OCaml的基本类型包括了字符型(char)、整型(int)和浮点型(float)。但是这些类型之间不能直接兼容,例如,不能把char看成是8位整数。但是,可以通过一些预定义的函数进行类型转换。在C语言中,字符串被看成是字符数组,是一个结构类型,但是在OCaml中,字符串是基本类型string。此外,布尔型(bool)也是OCaml中的一种基本类型,它只有两个元素true和false,不是1和0。

在OCaml语言中,除了数据有类型之外,程序也有类型。对于某些没有特别类型的操作,例如打印操作,OCaml专门设置了一个类型unit。1.4.1 整数类型int

OCaml整数常数的类型是int。也就是说,当我们写下一个整数常数时,系统会自动把它的类型判定为int类型。例如:# 3 ;;- : int = 3

在int类型上的四则运算操作符是:+,−,*,/。此外,还有取模运算mod。它们都是二元中缀操作符。每个操作都要作用在两个int类型的整数上,运算的结果也属于int类型。# 3 * -4 ;;- : int = -12# 5 / 2 ;;- : int = 2# 5 mod 2 ;;- : int = 1

整数除法的结果依然是整数。如果要得到更精确的除法结果,需要使用浮点数,见1.4.2节。除法操作“/”和取模操作“mod”之间的关系是:a = (a / b) * b + (a mod b)

除了用十进制方式书写整数常数外,还可以用十六进制、八进制和二进制的方式书写整数常数。十六进制整数以0x或0X开始,八进制整数以0o或0O开始,二进制常数以0b或0B开始。这里的“0”是数字,“o”和“O”是字母。为了提高可读性,数字之间可以使用下划线。在OCaml会话中,无论用哪一种进制做输入,输出结果都是十进制。例如:# 0x1 ;;- : int = 1# 0Xa1 ;;- : int = 161# 0O5 ;;- : int = 5# 0b0010 ;;- : int = 2# 0b1001_0010 ;;- : int = 146

关于int类整数的输入和输出函数在后面的章节中会详细讲解。

在32位机中,int中的数实际上是31位带符号整数,而不是32位整数。因此,32位机int类型整数的范围是。在64位机中,int中的数是63位带符号整数。int留出的一位在存储管理程序中使用。

符号常量max_int和min_int分别表示int中的最大整数和最小整数。在32位机上这两个数值是:# max_int ;;- : int = 1073741823# min_int ;;- : int = -1073741824

当整数计算超出了这两个值的范围时,计算结果会发生错误,但并不会造成系统发生意外终止。例如:# max_int + 1 ;;- : int = -1073741824# max_int * 10 ;;- : int = -10

如果要进行真正的32位整数计算,可以使用库int32(库模块名的首字母需要大写)。如果要进行64位整数计算,可以使用库int64。32位整数类型是int32,64位整数类型是int64。此外,本机整数的类型是nativeint,它可以是32位整数,也可以是64位整数,取决于机器字长。OCaml没有专门的8位和16位整数类型。char不能当作8位整数使用。

有些类型的数的库中设置了两个基本整数常数zero和one,例如,int32中的零要写成int32.zero,int64中的零要写成int64.zero。不同数类型之间需要用专门的函数进行相互转换。例如int64类型的数转移到int类型的函数是int64,如int(超过int范围的数在转换中会损失信息,详请可查OCaml手册),从int类型到int64的转换函数是int64.of_int。整数都是带符号的(signed),没有无符号的(unsigned)整数。

OCaml 3.07 版之后通过整数常量加后缀的方式表示不同类型的整数。32位整数的后缀是“l”,64位整数的后缀是“L”,本机整数的后缀是“n”。下面是几个例子:# 99l;;- : int32 = 99l# 99L;;- : int64 = 99L# 99n;;- : nativeint = 99n

如果要进行任意精度的整数计算,可以使用库Big_int。1.4.2 浮点类型float

OCaml只有一种浮点数类型float。它是双精度64位浮点类型,相当于C语言中的double。OCaml语言没有C语言中的32位浮点类型。

浮点常数必须带一个小数点“.”,否则会被视为int类型。例如:# 1. ;;- : float = 1.

浮点数中可以使用字母e表示指数。例如:# 3e2 ;;- : float = 300.

注意:数值表达式中表示指数的字符“e”后面不能带括号。# 1e(-10) ;;Characters 0-1: 1e(-10);; ^Error: This expression has type int This is not a function; it cannot be applied.# 1e-10 ;;- : float = 1e-010

当用户输入的数中包含小数点“.”或者表示指数的字符“e”或“E”时,系统就会认为是浮点数。

int类型和float类型是两个不相交集合,一个数不能同时具有这两种类型。系统在这两种类型间不能做自动转换。可以用转换函数进行显式类型转换,float_of_int将int类型转换为float类型;int_of_float将float类型转换成int类型。

在做浮点四则运算的时候,所有算术操作符后面都要加上小数点“.”:# 4e2 *. 2. /. 3. +. 1. ;;- : float = 267.66666666666669

如果把整数运算和浮点运算混合在一起,OCaml会报告类型错误:# 1 + 2. ;;Characters 4-6: 1 + 2. ;; ^^Error: This expression has type float but an expression was expected of type int

也就是说,OCaml中的算术操作符没有重载(overloading)机制。重载是指同一个操作符可以作用到不同类型的数据上。

当需要进行整数和浮点数混合计算时,需要在整数和浮点数之间进行转换。例如:# (float_of_int 1) +. 2. ;;- : float = 3.# 1 + (int_of_float 2.6) ;;- : int = 3# let a = 10 * 2 in (float_of_int a) -. 10.2 ;; - : float = 9.8

虽然这样做看起来比较繁琐,但实际编程时并不是一个严重的负担。为什么OCaml中的算术操作符不能实现重载呢?主要是

因为没有找到好的类型推导方法。在C这样的需要显式类型

说明的语言中,每个变量的声明都包含了变量的类型。例如,

在函数int f (int a) { return a+1 }中,a具有整数类型;在函数

float g (float b) { return b+2 }中,b具有浮点类型。因此,可

以推导出a+1是整数类型,b+2是浮点类型。在这当中,第

一个+1是整数操作,第二个+2是浮点数操作。在OCaml

中,使用变量之前可以不用说明变量的类型。例如,我们可

以直接写一个函数定义let f a = a + 1(类似于数学中定义函

数f(a)=a+1)。如果允许重载,这里就无法判定a的类型,也

无法断定a+1是整型还是浮点类型。因此,OCaml规定1只

能表示整数1,不能表示浮点1,“+”只表示整数加,不能

表示浮点加。由此保证类型推导能够顺利完成。

有很多数学函数只能作用于浮点数,不能直接作用于整数。这些函数有:平方根函数sqrt、正弦函数sin、余弦函数cos、以e为底的指数函数exp、以e为底的对数函数log(大部分数学教材中写为ln)、正切函数tan、反正切函数atan、反正弦函数asin、反余弦函数acos,等等。下面是几个例子:# sqrt 2. ;; - : float = 1.41421356237309515# sqrt (float_of_int 2) ;;- : float = 1.4142135623730951# acos (-1.) ;;- : float = 3.1415926535897931

注意,在Try OCaml中,“-1.”要写成“-.1.”,否则出错。在Linux版的OCaml和Windows版的OCaml中,这是正确的写法。

下面是稍复杂一点的浮点运算例子:# let x = sin(1.) and y = cos(1.) in x *. x +. y *. y ;;- : float = 1.1.4.3 字符类型char

字符类型char包含0~255个ASCII字符,每个字符必须写在两个单引号“'”之间。# 'x' ;;- : char = 'x'

每个字符用一个8位整数实现,但是在OCaml上不能把字符看成8位整数,在字符类型上没有定义算术操作。使用函数int_of_char可以查看字符对应的ASCII码,函数char_of_int则把ASCII码转换到字符:# int_of_char 'x' ;;- : int = 120# char_of_int 120 ;;- : char = 'x'# char_of_int (int_of_char 'x') ;;- : char = 'x'# char_of_int ((int_of_char 'x') + 1) ;;- : char = 'y'

char_of_int只对范围内的整数有定义,超出这个范围则会出错:# char_of_int 256 ;;Exception: Invalid_argument "char_of_int".

库Char提供了一些char类型上的有用的函数。例如,把字符变成小写字符或大写字符:# Char.lowercase_ascii 'A' ;;- : char = 'a'# Char.uppercase_ascii 'a' ;;- : char = 'A'

注意,这是OCaml 4.03版之后新增的函数。之前同样功能的函数是lowercase和uppercase。旧函数使用ISO Latin-1字符集,新函数使用US-ASCII字符集。

Char中还有两个函数Char.chr和Char.code,使用方法和作用效果分别与函数char_of_int和int_of_char一致。1.4.4 unit类型和简单输入输出

纯函数所做的工作是从参数输入到返回值之间的计算,其他工作一概不做。OCaml语言在纯函数式语言的基础上加入了一些属于命令式语言的“语句”,例如打印、赋值、循环等。“语句”的特点是:不产生输出值,但在运行过程中产生某种“副作用”。所谓“副作用”,指的是输入输出,修改系统状态等非函数型操作。为了把这些“语句”融合到函数式语言当中,就要把每一种“语句”改造成函数,因此,就要保证每一种“语句”既有参数输入,又有返回值输出。可是,有些“语句”原本没有参数,也没有返回值,例如打印新行的操作。为了解决这个问题,就引入了unit类型,它只含有一个值“()”。# () ;;- : unit = ()

有了这个类型之后,就可以让所有的“语句”都具有类型unit,语句的运行产生一个值“()”。这些函数有点类似于C语言中输出为void的函数。不过,void函数完全不产生输出,但具有unit类型的函数有一个实实在在的输出。例如,打印字符的函数print_char:# print_char 'a' ;;a- : unit = ()

在第二行的输出中,最左边的a是print_char打印的结果。由于没有打印换行,后面的系统输出也出现在同一行。

注:截至2018年1月,在线Try OCaml有一个bug,在交互过程中print类的语句的打印结果不出现,并回应“_:unit=”。

此外,还有打印整数的函数print_int,打印浮点数的函数print_float和打印新行的函数print_newline。由于所有的函数都必须有一个输入参数,print_newline也不例外,所以规定它的参数就是unit类型的()。一般而言,凡是不需要参数的函数都使用()作为参数。打印操作可以顺序执行,每个操作之间用分号“;”分开:# print_int 2 ; print_char ' ' ; print_int (-3) ; print_newline () ;;2 -3- : unit = ()

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载