Programming Languages PartA Week2学习笔记——SML基本语法


首先简单介绍使用的SML语言,参考维基百科和百度百科:

MLMeta Language:元语言)是由爱丁堡大学的Robin Milner及他人在二十世纪七十年代晚期开发的一个函数式、指令式的通用的编程语言,它著称于使用了多态的Hindley–Milner类型推论。ML能自动的指定多数表达式的类型,不要求显式的类型标注,而且能够确保类型安全,已经正式证明了有良好类型的ML程序不会导致运行时间类型错误。

ML提供了对函数实际参数的模式匹配、垃圾回收、指令式编程、传值调用和柯里化。它被大量的用于编程语言研究之中,并且是全面规定了的和使用形式语义验证了的少数语言之一。它的类型和模式匹配使得它非常适合并且经常用于在其他形式语言上进行操作,比如在编译器构造、自动化定理证明和形式验证中。

Standard MLSML),是一个函数式、指令式、模块化的通用的编程语言,具有编译时间类型检查和类型推论。它流行于编译器作者和编程语言研究者和自动定理证明研究者之中。

Standard ML是ML的现代方言,ML是用于LCF(可计算函数逻辑)定理证明计划的编程语言。Standard ML在广泛使用的语言之中与众不同,源于它具有正式规定《The Definition of Standard ML》,给出了语言的类型规则和操作语义。

ML一般被归为非纯函数式编程语言,因为它允许副作用和指令式编程。命令式语言以语句作为基本单位,ML遵循这一规则,但同时继承了函数式语言的很多特征,所以ML属于一种具备命令式语言特点的函数型语言,或者说面向函数的命令型语言。

SML又存在有多种实现:

本博客采用Standard ML of New Jersey实现方式。


本博客主要记录Coursera上华盛顿大学的Programming Language,PartA课程的学习过程,主要是学习笔记和个人思考,不一定成体系,也不一定完全对。欢迎批评指正!

本课程使用ML这种语言来入门函数式编程是合理的,因为ML同时具有函数式和命令式特点,但以命令式为基础,写起来会比较亲切(对于以C等命令式语言入门编程的人来说,尽管函数式编程的思路和面向过程、面向对象的编程思路都不太一样)

基础语法可以参考其他博客,例如:

SML基本语法(一)

SML基本语法(二)

SML基本语法(三)

SML基本语法(五)

或书籍,例如:

Programming in Standard ML (DRAFT: VERSION 1.2OF 11.02.11.)By Robert Harper, Carnegie Mellon University, Spring Semester, 2011

Week 2

Variable Binding

image-20220421172237025

type checking before evaluation

2image-20220421173119561

变量绑定:

1、type checking -> 对变量的数据类型进行绑定 -> static environment

2、valuation -> 对变量的数值进行计算 -> dynamic environment

变量绑定的时候可以是某一个数值,也可以是一个表达式(if then else)

val x = 1
val y = if x = 1 then true else false

Rules for Expressions

Three things to think about for expressions:

Syntax: how we write down this expression

Valid types: what types are allowed for subexpressions , and what type this expression returns

Evaluation: how we run this when it's part of a program

image-20220422101154255

老师给出的例子:

Syntax:
	if e1 then e2 else e3
	where if , then , and else are keywords and 
	e1, e2 and e3 are subexpressions
Type-checking:
	first e1 must have type bool
	e2 and e3 can have any type(t), but have same type(t)
	the type of the entire expression is also t
Evaluation rules:
	first evaluate e1 to a value(v1)
	if v1 is true, evaluate e2 and that result is the whole expression's result
	else, evaluate e3 to be the whole expression's result

REPL and Errors

REPL: Read-Eval-Print-Loop

REPL stands for 'Read Evaluate Print Loop.' The REPL can be used to write and execute code one line at a time and is an alternative to writing code to a file and then compiling or interpreting the entire file before execution

当我们使用use xxx.sml 命令执行文件时,实质上是对文件中的每行代码放到REPL进行批处理

REPL类似于Python的交互模式

image-20220423095955026

易错点1:if e1 then e2 else e3 语句中,e1必须为bool,且e2与e3类型相同

易错点2:SML不能使用负号表示负数,如-5,需要使用0-5的表达式,或者~5的形式

易错点3:没有类似C语言的隐式自动类型转换,整数除法需要两个int使用 div (返回int),实数除法需要两个real使用/(返回real)

易错点4:SML使用=表示相等(逻辑表达式),而不是“==”

混用数据类型可能报错,例如:

image-20220424113208318

易错点4:与python类似,SML在除数为0时会抛出异常 uncaught exception Div [divide by zero]

Shadowing

值得注意的一点是,SML中“=”既用于变量初始化赋值和Shadowing,也被用作逻辑运算“等于”的符号

1、**数据类型相同时****的Shadowing,与其他语言的重新赋值类似,Shadowing后的变量不会影响已有变量的值,例如:

(* variable value shadowing *)
val x = 12;
(* x -> int *)
(* x -> 12 *)

val y = x+8;
(* y -> int *)
(* y -> 20 *)

val x = 250; (* shadowing *)
(* x -> 250 *)

val z = y;
(* z -> 20 *)

image-20220424111159832

2、数据类型不同时的Shadowing,类似Python改变变量的引用对象,可以赋值不同类型,例如:

(* variable type shadowing *)
val i = 1;
(* i -> int *)
(* i -> 1 *)

val i = "123";
(* i -> string *)
(* i -> "123" *)

image-20220424113832494

Functions

与大多数语言类似,函数需要先声明才能调用,

(1)函数声明或函数绑定(Function binding),SML的函数以“=”连接函数名和函数内容(表达式),没有return关键字,例如:

(* syntax *)
fun functionName(arg0 : type0, arg1 : type1, ...) = expression
(* examples *)
fun pow(x:int,y:int) = if y=0 then 1 else x*pow(x,y-1)
fun cube(x:int) = pow(x,3)

image-20220504214210553

值得注意的一点是,与Java等语言不同,SML的函数声明同时,函数本身就作为一个value被添加到dynamic environment当中,同时,函数将具有 (t1 * ... * tn) -> t 的类型(type-checking),其中的“*”表示连接两种类型,例如下面运行结果:

image-20220504213016365

函数如果没有参数,他的类型就是unit -> t

(2)函数调用(Function call),例如:

val sixtyfour = cube(4) 
(* 函数调用时的括号不是必须的,如果只有一个参数,上式也可用空格分割 *)
val sixtyfour = cube 4

image-20220504215315705

image-20220504215456243

值得注意的是,e1 ... en都可以是一个表达式,在evaluate时计算其值;e0是一个函数,在evaluate时会被关联到相应的函数上进行计算

Pairs and Other Tuples

1、Pairs(2-tuples)

相当于一个二元tuple,其中能容纳两个元素(e1,e2)

假设e1 -> type1 ,e2 -> type2 ,则

(e1,e2) 具有 (type1 * type2),有时也可以不加括号

image-20220504220405559

image-20220504220631397

Pairs的建立和取出元素值,例如:

(* 函数表达式的括号内建立了一个新的pair *)
(* int*int -> int*int *)
fun div_mod(x:int,y:int) = 
	(x div y , x mod y)
	
(* 取出pair的元素值 *)
(* (int*int)*(int*int) -> int *)
fun sum_two_pairs(pr1:int*int, pr2:int* int) = 
	(#1 pr1) + (#2 pr1) + (#1 pr2) + (#2 pr2)
	
(* pair元素可以不同类型 *)
(* int*bool -> bool*int *)
fun swap(pr:int*bool) = 
	(#2 pr,#1 pr)

2、Tuples

更一般的,多个元素用小括号括起来就是tuple,binding规则和元素获取方法与pairs类似

image-20220504221721447

Lists

列表绑定:

type chcking:

? 假设v1,...,vn都具有type t

? [v1,v2,...,vn] -> t list

valuate:

? 计算每个v表达式的值a

? 得到 [a1,a2,...,an]

(* 空列表类型为 'a list,无法确定具体类型时就是'a *)
w = []
(* w -> 'a list *)

x = [1,2,3]
(* x -> t list *)
(* x -> [1,2,3] *)

y = [[1,2],[2,3],[5]]
(* y -> t list list *)
(* y -> [[1,2],[2,3],[5]] *)

z = [(1,2),(2,3)]
(* z -> (int*int) list *)

image-20220506194810571

值得注意的是:e1::e2 ,e1是一个元素, e2是一个列表,不能用于列表连接列表,列表连接列表需要递归实现或者使用 e1 @ e2

类似的连接两个字符串使用 ^ 符号

获取列表数据:

值得注意的是:null是一个函数,其类型是 'a list -> bool,用来检测列表是否为[]

image-20220506202703858

hd 取出列表的第一个元素,其类型是 ‘a list -> 'a

tl 取出列表除第一个元素外的子列表,其类型是一个 'a list -> 'a list

需要注意的是,list前的type如果是元组之类的,需要用括号括起来,例如 (int*int*int) list,表示三元元组的列表

List Functions

由于Lists的取值需要用到hd(取第一个元素)和tl(取除第一个元素外的子列表),想要遍历列表需要不停的调用这两个方法,因此递归(Recursion)在List调用中是一种非常重要的方式。

例如:

fun sum_list(xs : int list) = 
	if null xs
	then 0 
	else hd xs + sum_list(tl xs)

(* (int list) * (int list) -> int list *)
fun append(xs:int list, ys:int list) = 
	if null xs
	then ys
	else (hd xs)::(append((tl xs),ys))

fun sum_pair_list(xs:(int*int) list) = 
	if null xs
	then 0
	else #1(hd xs) + #2(hd xs) + sum_pair_list(tl xs)

fun firsts(xs: (int*int) list) = 
	if null xs
	then []
	else (#1 (hd xs))::firsts(tl xs)

fun seconds(xs: (int*int) list) = 
	if null xs
	then []
	else (#2 (hd xs))::seconds(tl xs)

fun sum_pair_list2(xs: (int*int) list) =
	(sum_list (firsts xs)) + (sum_list (seconds xs))

image-20220508152843645

基本语法回顾1

image-20220509170202640

image-20220508153413627

Let Expressions

作用域Scope:在绑定之后的部分和let中的表达式部分可以调用

image-20220509170253089

Let 关键字用于Local Binding,类似于局部变量声明。用在函数内部可以提升函数的灵活性(否则函数就只能通过if then else的基本语句来赋值)。如果把函数中的表达式的值类比为其他语言的return的值,那么Let让SML的函数也能像其他语言的函数一样有一个局部作用域(Local Scope),定义局部变量以在函数表达式中使用。

image-20220508162105467

Let中表达式部分是body部分,其值为整个Let语句的值。

fun silly1(z:int) = 
	let 
		val x = if z>0 then z else34
		val y = x+z+9
	in
		if x>y then x*2 else y*y
	end

(* 表达式所属的最近let定义了其局部作用域 *)
fun silly2() = 
	let
		val x = 1
	in 
		(let val x = 2 in x+1 end) + (let val y = x+2 in y+1 end)
	end

image-20220508195750672

Nested Functions

嵌套函数,类似JavaScript的嵌套函数,子函数在函数内部定义,只能在函数内部调用。函数内部定义了一些内部binding,则子函数可以看做闭包,子函数可以使用其位于的作用域内的其他binding。

image-20220508212100335

例如:

fun countup_from1(x:int) = 
	let 
		fun count(from:int, to:int) = 
			if from=to
			then to::[]
			else from::count(from+1,to)
	in 
		count(1,x)
	end

(* Better version *)
fun countup_from1(x:int) = 
	let 
		fun count(from:int) = 
			if from=x
			then x::[]
			else from::count(from+1)
	in 
		count(1,x)
	end

函数设计中可重用性的代价:代码重用可以节约时间并减少bug,但可重用的代码更难适应改变。

image-20220508213003381

Let and Efficiency

由于SML遍历列表的方法必须用到递归,但多次调用递归会导致算法的时间复杂度大幅度增长,例如下面的例子在函数中用了两次递归:

image-20220508214908433

image-20220508215710146

image-20220508220527142

Buying a faster computer won't help much!!! img

使用Let保存递归的返回值,能够防止反复调用相同的递归,例如:

image-20220508220130901

Options

由于SML中处理空列表或一个元素的列表是很常见的,之前往往通过随意赋值,例如空列表的情况为表达式赋值0或1或者[]来防止异常产生。为了更严谨和简化处理这一问题,使用Options。

image-20220509104155859

在实际使用中需要注意 t option是一种类型,函数如果在处理空列表时返回NONE,那么在非空时也应该返回一个 t option(例如下面例子中的SOME (hd xs) ),同时递归时的返回值当然也是t option,所以tl_ans也是一个t option(取值的时候先isSome判断再valOf取出其中的值)。由于函数返回t option,在调用函数时也需要使用valOf获取返回option的值。

image-20220509110139133

为了尽可能减少isSome方法的调用,第二种max实现方式是使用嵌套函数,这种方式实质上与上一节的good_max函数实现类似,只是有两点不同:

(1)将空列表判断返回NONE,最大值返回SOME,整个函数返回值是int option

(2)将递归求非空列表最大值的部分封装成了一个子函数,子函数返回值是int

image-20220509111131948

Booleans and Comparison

逻辑运算符

andalso,orelse,not。其中andalso和orelse只是两个连接词,但not是一个函数

值得注意的是andalso和orelse具有与其他语言的and(&&)和or(||)类似的短路特性,即例如

val z = 3;
(* 此时由于z不等于2,所以(4+5)<10 不会被valuated *)
val x = if (z = 2) andalso (4 + 5 < 10)
        then 5
        else 0

image-20220509152521021

但这三个逻辑运算符不是必要的,因为均可以表示为SML中的基本表达式if then else的形式

image-20220509154054492

比较运算符

比较运算符

> < >= <=不能比较两个不同类型的值的大小,例如一个real和一个int不能比较,需要显式地类型转换:

(* 可以比较 *)
3 > 2
3.0 > 2.0

(* 不能比较 *)
3.0 > 2

(* 显式类型转换 *)
3.0 > Real.fromInt 2 

同时,与其他语言类似,相等和不相等可以用于任意两个可比较的内容比较是否相等,但不能用于real,因为real相当于其他语言的float,计算机没办法确定两个浮点数是否完全相等

image-20220509154351626

Benefits of No Mutation

SML中的变量值不能被更改(Shadowing只是改变了变量的引用),列表元组的元素也当然不能修改,下面是老师给出的说明不可更改的好处的例子:

image-20220509191620923

上图的第一个函数返回的pr是pr本身的别名(相当于C++里的引用的含义),而第二个函数返回的是另一个元组。这两个函数从外部看不出任何区别,但却会导致如下的问题:

image-20220509191703083

y是否是x的别名取决于函数内部的实现,这会导致后续修改时的不同,例如修改x可能导致y变化。当然,例如C++这类的语言如果在函数参数中仅仅传递数值是不存在这个问题的,但一旦传递指针或引用就需要格外小心返回的变量是否是原变量的别名。

所以SML中规定这些值不能够被修改,好处就在于我们不需要关注函数的内部实现是否会导致两个变量名指向同一个地址的问题,因为无法修改,所以其影响非常小。我们不需要考虑y是x的拷贝或是别名,相当于是屏蔽了变量的底层细节。下面的例子说明了同样的道理:

image-20220509194010818

以Java的引用作为反例,如下,可以看到如果获取到的变量是作为私有变量的一个别名(指向同一个地址),则可以从外部获取修改私有变量的值,这样带来了安全性问题:

image-20220509195014068

image-20220509195024451

Pieces of a Language

“编程语言的哲学”

学习一门语言的5项关注点:Syntax语法或句法、Semantics语义、Idioms习惯用法、Libraries库、Tools工具

image-20220509195409182

Syntax是语言的基础,但学习重点在于关注Semantics和Idioms(以及其中表现出的编程思想),Libraries和Tools在实际使用这门语言时再重点关注

image-20220509195829199

SML