考完就不看のSML语言基础
来自于educoder
SML语言中的基本类型
概述
SML语言中基本类型有bool, int, real, char, string和function(函数类型),一个数据的类型可以是布尔值,整数,实数,字符(串),甚至是函数。
通过这些基本类型可以组合出其他的类型,例如,一个含有两个元素的表(二元组)可以表达为(13,"Hello, world!"),其中第一个元素的类型为int,第二个元素的类型为string,整个二元组的类型为string * int,注意中间的*并不是乘号,而是一个连接符号。
提示:("John",18)和(18,"John")不是一个类型,前者为string * int,后者为int * string。
int类型
请打开页面给出的命令行窗口,键入sml命令,即可进入SML交互式环境。
此时,屏幕会弹出
Standard ML of New Jersey v110.78 [built: Thu Jul 23 11:21:58 2015]
表示你已经进入了SML的开发环境。不过,在调试之后,请把要用的代码复制到提交的文本框内,系统才能对其进行编译和评测。
我们可以通过
val a = 15;
来创建一个名称为a,值为15的整型变量,其中,val为SML的关键字,表示使用名字a代替15,语句末尾需要加上分号;表示结束。
SML通常会返回如下内容
> val a = 15 : int
我们以后在代码前加一个符号>,表示编译器返回给我们的结果。
如果你想创建一个负数,请输入
val b = ~15;
注意前面的符号是~而不是负号-,SML对运算符有着极为严格的约束,符号-为减法的中缀运算符,只能且必须接受两个参数。符号~为取相反数的运算符。或者,你也可以输入
val c = 0 - 15;
此时程序输出
> val c = ~15 : int
表示你创建了一个整数c,值为-15。
提示:int类型的上界为2^{30}-1=1\ 073\ 741\ 823230−1=1 073 741 823,下界为-2^{30}=-1\ 073\ 741\ 824−230=−1 073 741 824。
接下来,如果你想做整数的计算,我们有算符+, -, *, div, mod,其中特别注意整数除法a除以b为a div b,a模b为a mod b。
real类型
SML中的带符号小数使用real类型,你可以通过
val pi = 3.14159;
创建一个名为pi的实数变量,程序通常会返回
> val pi = 3.14159 : real
其中,real表示变量的类型为实数。
另外,如果你想创建一个绝对值为1的实数,必须输入
val k = 1.0;
小数点是不可少的,如果没有小数点,编译器会认为k是一个int类型而非real类型。
另外,定义在real上的算符有+, -, *, /,注意实数除法a除以b为a/b而不是a div b,和整数是不同的。
我们要特别指出,两种不同的数据类型之间不能进行任何运算,例如,一个整数无法和一个实数相加。如果你想要用一个整数加一个实数,需要把整数转换成实数,或实数转换成整数,对于整数转换为实数,我们有
Real.fromInt(4)
它可以把整数4转换为实数4.0。
对于实数转换为整数,我们有
floor(1.4);ceil(1.4);trunc(~1.4);
它们的功能请自行实验。
string和char类型
文本是由字符组成的串,它的类型为string,字符串常量是用双引号括起来的:
val s = "Madam, I'm Adam";
用符号^可以连接两个字符串,如
val s1 = "Hello" ^ "world!";
程序会输出
> val s1 = "Helloworld!" : string
注意程序并不会特地帮助你在两个字符串之间插入一个空格。
bool类型
布尔类型只可能取两个值,如下:
val a = false;val b = true;
如果试图把a的值设为0,那么编译器将认为a是一个整数而非布尔数,另外,布尔数true, false只能和自身比较,试图比较true与1的关系将会产生编译错误。
输出
在整个编程实践中,我们主要将目光放在如何利用SML语言编写一个算法上,输入和输出的函数程序会预先给定,我们只需要调用即可。
int的输出
我们已经预先给出了printInt函数,调用
printInt(25);
就可以在屏幕上输出25和「一个空格」,不过并不需要担心在测评过程中出现什么问题,我们已经忽略了行末可能多出来的那个空格。另外,我们输入
printInt 25;
也可以在屏幕上输出25,括号并不是必须的。
real的输出
我们也预先给出了printReal函数,不过考虑到精度问题,我们会尽量避免输出实数。总之,你可以使用
printReal(25.0);
在屏幕上输出25.0和一个空格,另外,同样地,括号也不是必须的。
string的输出
你可以使用printString来输出一个string,当然,直接使用内置函数print也可以。
printString("Hello, world!");
输入两个整数
我们预先给定了函数
getInt();getReal();
来输入整数和实数。
可以通过
val m = getInt();
从标准输入读取一个正整数mm。
使用条件表达式
为了分情况定义函数,或者说定义分段函数,我们可以引入条件表达式和测试表达式。
测试表达式
一个测试表达式EE是像这样的东西
a = 4;b > 5;c <= 6;
这个表达式返回一个bool类型的值,true表示表达式成立,false表示表达式不成立。
一些简单的判断关系有=, >, <, >=, <=, <>,其中最后一个表示不相等。特别注意这里判断相等是一个等号,不等是一个小于号和一个大于号(而不是!=)。这些关系可以用来判断整数或实数。
判断表达式
一个判断表达式是像这样的东西
if E1 then E2 else E3
其中,else不能省略,E1是一个测试表达式。
这个表达式的作用是如果E1为true,那么把这个位置的式子替换为E2,否则把它替换为E3。
例如一个计算n的符号的函数可以写成这样:
fun sign(n) = if n > 0 then 1 else if n = 0 then 0 else ~1;
注意分号的位置,与C语言有所不同,实际上,把这四行代码写在一行里,也不会有任何问题。
定义一个函数
在SML中,一个函数的定义大概像下面这个样子
fun area (r : real ) = 3.14 * r * r;
函数定义以关键字fun开始,而area是函数的名字,r是形式参数,或者可以简单理解为这个函数的参数,冒号后接类型名,表示限定r必须为实数,不过如果不接类型名,编译器会自动推导参数的类型。但是不能保证推导结果一定准确。
=之后的部分为这个函数所代表的公式(注意前面我们提到的,相比起C语言中一个函数是一个依次执行的过程,在SML中一个函数是一个数学公式,我们不过是把参数「代入」了这个公式),或者说,把area(r)「替换」成3.14 * r * r。当然,它和替换有一点点小区别,但是我们这么理解是无妨的。
另外,函数的括弧不是必要的,我们可以写成
fun area r = 3.14 * r * r;
和上面的函数具有相同的作用。如果我们要定义一个接受多个参数的函数,如
fun plus(a, b) = a + b;
此时这个函数并非接收了两个参数,而是接受了一个二元组,这个数组中的两个元素在函数中有各自的作用,但总体来说函数只接收了一个「对象」,或者说一个“东西”。
我们要特别指出,我们可以反复定义一个函数,例如这样
fun area (r) = 3.14 * r * r;val a = area(5.0);fun area (r) = 3.14 * r;val b = area(5.0);
那么,对于这个例子,a的值将为78.5,而b的值将为15.7。
无限精度整数的使用
无限精度整数类型为
IntInf.int
我们在声明一个数值的同时,显式指定类型,就能声明该类型的数据,比如这样
val a : IntInf.int = 2147483648;
我们声明了一个无限精度的整数aa,它的大小是2^{31}231,已经超过了普通整数的上界。程序输出如下结果
val a = 2147483648 : IntInf.int
接下来,我们计算a的平方,因为是两个相同类型的数据相乘,无限精度整数也定义了它自己的乘法符号
val b = a * a;
程序输出如下
val b = 4611686018427387904 : ?.intinf
(注意这个版本的编译器可能有一些bug,产生了一些乱码,但是不影响使用)
接下来,我们的快速幂将在无限精度上完成。
特别地,我们新增了getIntInf函数和printIntInf,用来帮助你读取和输出大整数。它的使用方法像这样
val b = getIntInf();printIntInf(b);
迭代与递归
你或许在C语言里编写过这样的代码
int sum = 0;int NumberCount = 0;scanf("%d", &NumberCount);for(int i = 0; i < NumberCount; i++) { int tmp; scanf("%d", &tmp); sum += tmp; }print("%d\n", sum);
我们很容易发现,它的作用是计算一组数据的和。但是,在ML语言里,你可能很难写出这样的代码,为什么呢?
你也许会觉得,ML里舍弃这种循环的写法,而是必须生硬地写成递归形式,是很没有道理的。但是,一个设计良好的递归算法,比如求这一组数据的和的算法,很容易划分为求两个(相等长度的)子序列的和,再把它们相加。于是,用迭代的方法,算法的复杂度为
但是利用递归,我们可以做到
它不仅是 的,而且可以在并行计算机上实现更低的复杂度。
数据结构:表
如果我们的计算机只用处理几个数据,这时我们可以把它们分别命名并进行处理,但是有时候要求输入一组数据到计算机中,这些数据有某种共性,并且各自具有差异性。对于这类数据,我们往往会把它们放到C语言的数组里,就像这样
for(int i = 0; i < NumberCount; i++) scanf("%d", & Array[i]);
可以做成表的数据多种多样,例如一个学号表,一张质数表,或者一个利用结构体构造的实例表,如
struct Student{ int IdentityNumber; enum Grade; string Name; int Year_of_birth;};Student Table[100];
这样就构造了一个包含100个学生信息的表。总之,表是一个常用且重要的数据结构。
但是,在C语言中,对表的处理仅仅是简单地划分了一块内存,类似
Student tmp = Table[102];
的语句在编译时很可能不会提示错误,但是却成为了运行时的灾难。甚至连语句
Student tmp = Table[-3];
也是合法的,毕竟,从C语言的角度看,我们只是对内存地址进行了相应的偏移而已。
但是,在SML中,表是作为一个数据结构出现的,和C语言有很大的区别(后者仅仅认为表是一块内存而已)。在学习SML中的表的时候,请舍弃C语言中数组的概念。某种意义上,SML中的表更像C++ STL中的list,远非仅仅一块内存。
「表」 (list) 是一个「有限」的元素序列,其中元素的顺序是有意义的,并且元素可以重复出现。类似的数据结构集合中,元素的顺序没有意义,并且元素只能出现一次(当然,我们并没有讨论多重集)。例如,以下的表都是不同的
[3,4] [4,3] [3,4,3] [3,3,4]
表中的元素的类型可以是任意的,比如整数、实数、字符串或布尔值,但是表中每个元素的类型必须相同(对比元组来看),例如
(3, 3.0)
会被认为是一个二元组,第一个元素是整数3,第二个元素是实数3.0,但是一个表如果像下面这样
[3, 3.0]
那么编译器就会提示错误。
除了类型上有限制,表和元组还有一个重要的区别:表的长度是可变的。这一点和C语言中的表有很大不同,SML中的表实际上仅仅是一些元素连接起来构成的,例如
nil
表示一个空表,也可以用[ ]表示一个空表。
3 :: nil
中间的::符号称为表的构造符号,这个表达式产生一个表,表中的元素只有3,大概像这样的感觉:
[ 3 ]
如果你输入一个表[3,5],实际上和输入
3 :: 5 :: nil
等价。
最后,我们给出表的递归定义:
一个表L要么是空表,要么形如x :: L1,其中L1也是一个表,x是一个数据元素。
所以,每个表最后必须以空表结尾,类似
3 :: 5
并不能构造一个表,编译器只会弹出一个错误,但是你可以通过3 :: 5 :: nil或3 :: 5 :: []来构造一个表。
几个内置函数
我们给出表的三个内置函数null, hd和tl
其中
null
判断一个表是否为空表,例如
null [10];> val it = false : boolnull [];> val it = true : bool
hd
给出一个表的表头,例如
hd [1,2,3];> val it = 1 : int
特别注意,
hd
在接受一个空表作为参数的时候,会产生异常。
tl
给出一个非空表的表头后面的那部分,例如
tl [1,2,3];> val it = [2,3] : int listtl [1];> val it = [] : int listtl [];> uncaught exception Empty
注意
tl
的返回值总是一张表。
定义在表上的库函数
我们先给出三个函数,分别是length,take和drop。
length函数接受一个表作为参数,返回这个表的长度,例如
length([1,2,3]);> val it = 3 : intlength([]);> val it = 0 : int
时间复杂度为
take函数定义在List结构里,接受两个参数,第一个参数是一个表,第二个参数是一个整数N,会返回另一个表,包含这个表的前N项,例如
List.take([1,2,3],2);> val it = [1,2] : int listList.take([1,2,3],0);> val it = [] : int listList.take([1,2,3],5);> uncaught exception Subscript [subscript out of bounds]
drop函数定义在List结构里,接受两个参数,第一个参数是一个表,第二个参数是一个整数NN,会返回另一个表,包含弃去这个表前NN项后剩下的表,例如
List.drop([1,2,3],2);> val it = [3] : int listList.drop([1,2,3],1);> val it = [2,3] : int listList.drop([1,2,3],3);> val it = [] : int listList.drop([1,2,3],4);> uncaught exception Subscript [subscript out of bounds]List.drop([1,2,3],0);> val it = [1,2,3] : int list
这两个函数的时间复杂度均与length相同。
另外,还有一个函数nth,它可以取出这个表的第nn项,从00开始计数。用法像这样
List.nth(["Hello","world","You"],0); > val it = "Hello" : string
追加和翻转
你可以使用SML中内置的方法把一个表接到另一个表之后,或者将一个表内的元素翻转过来。
追加操作符@可以将一个表追加到另一表之后,像这样
[1,2,3] @ [2,3,3];> val it = [1,2,3,2,3,3] : int list[ ] @ [2,3,3];> val it = [2,3,3] : int list[1,2,3] @ [ ];> val it = [1,2,3] : int list[1,2,3] @ [4.0];> Error: operator and operand don't agree [overload conflict]
注意要追加的两个表内部的数据类型必须相同,否则会产生错误。
另外,我们可以使用rev函数翻转一个表中的元素,像这样
List.rev([1,2,3]);> val it = [3,2,1] : int list
注意如果你试图翻转一个空表,结果仍是一个空表,同时会产生一个警告。
表的配对
是不是觉得一直都在介绍表的操作,有些无聊了?不用担心~从下一题开始就是表的应用了。
如果你有两张表,一张表上记录了学生的学号,另一张表上记录了学生的姓名,你或许会希望把每一个学号和一个姓名配对起来,像这样
val Name = ["Three Zhang", "Four Lee", "Five Wong"];val IdentityNumber = [1000,1001,1002];
你想生成一张新表,内容大概是这样的
[ ("Three Zhang", 1000), ("Four Lee", 1001), ("Five Wong", 1002) ]
那么,你可以使用ListPair库中提供的zip函数,像这样
ListPair.zip(Name,IdentityNumber);> val it = [("Three Zhang",1000),("Four Lee",1001),("Five Wong",1002)] : (string * int) list
你可以在这里找到关于结构List的更多内置函数,在这里找到关于结构ListPair的更多内置函数。
向量的使用
SML中的表,类似C语言中的链表,你只能从前往后逐个访问,访问某个表中元素的开销为O(N)O(N)。不过在SML中我们也提供了可以随机访问的表vector,你可以利用函数Vector.fromList从表中创建一个向量。
val vec = Vector.fromList [1,2,3];> val vec = #[1,2,3] : int vector
特别要注意的是,在交互式环境中,你可以用
val vec = #[1,2,3];
直接创建一个vector,但是我们的评测环境不支持这种做法,请不要这样使用。
我们提供了getIntVector函数和getIntInfVector函数,方便你直接从标准输入读入一个vector。
接下来,你可以使用Vector.length来求出一个vector中含有的元素个数,你可以使用Vector.sub函数取出一个vector里的任意元素,下标从00开始。这个操作是O(1)O(1)的。
Vector.length(vec);> val it = 3 : intVector.sub(vec,1);> val it = 2 : int
你可能会问,为什么我不能用List.nth完成这个问题呢?因为这个操作是O(N)O(N)的而非O(1)O(1)的,List不支持随机位置读写,而Vector支持。
理论上来说,一个vector创建之后就保持不变了,最好不要更改其中元素的个数,不过,你可以修改其中某个元素的值,但是我们并不推荐这样做。
你需要利用vector结构完成二分查找问题。
map方法
map这个函数内置于SML中,作用是给定一个算符f和一组元素序列[x1, x2, ..., xn],计算[f(x1), f(x2), ..., f(xn)]的值,例如,我们有如下的简单小程序
fun getSum ( [] ) = 0 | getSum ( x::xs ) = x + getSum(xs);map getSum [ [1,2,3], [4,5,6,7] ];> val getSum = fn : int list -> int> val it = [6,22] : int list
这里,map函数把getSum分别作用在了[1,2,3]上和[4,5,6,7]上,计算出两个求和的值为6和22。这两个计算的过程是同时进行的。
SML中的命令式设计
引用及其操作
在ML中,关键字ref可以创建一个引用(reference),例如
val p = ref 5;> val p = ref 5 : int ref
表示分配一段内存空间,并在这段空间里存储整数5,然后把这段空间的地址赋给p。
你可以通过操作符!进行反引用,它取出对应内存地址中存储的值,例如
!p> val it = 5 : int
接下来,你可以通过赋值操作符:=对引用所代表的内存空间进行赋值,它和C语言中的赋值有所不同,SML中的赋值操作符两侧都可以是表达式,但C语言的赋值操作符左侧必须是变量名。SML中的赋值操作符要求左侧表达式求值的结果为一个内存地址(指针),右侧求值的结果为一个值,并且类型必须和左侧指针指向的值的类型相同。赋值操作符返回一个unit,所以并不能连续赋值。
总之,我们举几个例子:
p := 6;> val it = () : unitp;> val it = ref 6 : int refp = 6;> stdIn:5.1-5.6 Error: operator and operand does not agree [overload conflict]p = ref 6;> val it = false : bool- !p = 6;val it = true : bool
注意第7行,指针的相等当且仅当它们指向相同的内存地址时才成立,p有自己的内存地址,然而ref 6又创建了另一块内存空间存放6这个数,所以即使它们内存地址里的值是相同的,但是因为它们指向的内存地址不同,所以它们仍然不相等。另外,解引用之后从内存地址中取出的值是相等的。
当然,类似C语言中的多重指针,引用的引用也是允许的,不过除非必要,否则并不推荐这样做。
如果把一个引用赋给另一个引用会发生什么?
val q = p;> val q = ref 6 : int refp := 7;> val it = () : unitq;> val it = ref 7 : int ref
我们发现,当我们修改p的值之后,q的值也改变了,这是因为,你修改的是p所指向的内存空间中存储的值,而q和p指向相同的内存空间,所以它们的值会同时改变,这并不是一个好的设计模式,不过我们有时会用到这种设计方法。
当然,函数类型也可以声明为引用,不过我们在此不做讨论。
另外,在函数调用一个二元组(E1,E2)的时候,如果E1改变了状态,它可能会影响E2执行的结果,我们并不推荐这样设计。
命令组
你可以通过表达式
(E1; E2; ...; En)
来执行一组命令,它会从左到右按顺序计算E1, E2, ..., En的值,然而,整个表达式的结果是En的值,其他表达式的值都将被丢弃。由于分号操作符在ML中还有别的作用,所以这一组表达式必须用括号括起来,除非用在let表达式的主体中
let Din E1; E2; ...; Enend
迭代
ML给出了while命令进行迭代,命令语法是这样的
while E1 do E2
如果对E1的求值结果为false,那么命令结束。如果对E1的求值结果为true,那么对E2进行求值,并且再次执行while,精确地讲,while可以定义为一个递归方程
while E1 do E2 = if E1 then (E2; while E1 do E2) else ();
在这里,我们每次对E2求值,仅仅是为了改变某个状态。
本文使用 mdnice 排版