Abstract
最近在实验室实习的过程中学到了很多有意思的东西,对Programming Languages
也有了更深的了解,Ocaml
是一门支持多范式的函数式编程语言。程序描述计算,但是又不仅仅是计算。Ocaml
的学习带给我的不仅仅是语言本身,这个系列记录了其中的部分心得:
- 关于
scripting/imperative/object-oriented/functional PL
的基本概念Higher-order Functions
State-Full vs State-Free Computation
Modelling Objects and Closures
Exceptions/Continuations to Defer Control
Polymorphism
Partial Evaluation/Lazy Programming/Modules
- 关于
reason about programs
的方法Type Checking
Induction
Operational Semantics
QuickCheck
- 关于设计一门语言的基本规则
FP's Basic Concepts
Ocaml is a statically typed functional programminng language
。函数式编程中,function
永远是一等公民,程序中只有expression
的概念,通过functions
递归处理values
来完成computations
。Ocaml
通过pattern matching
模式匹配的方式来定义递归数据结构和函数,进而写出更简洁的程序。
在函数式中还有一个和imperative PL.
中不同的概念称为effect-free programming
。在命令式语言中,我们通过给variables/fields
赋值来修改程序状态并将赋值行为的结果看成是effects
。但是比如函数式就不会explicitly
的分配内存,甚至没有exception handling
来改变控制流状态——不支持effect
被称为pure functiional
比如Haskell
。Ocaml
不是纯函数式语言。所以支持effectful/state-full programming
和pure FP
。
最后一个关键字就是statically typed
静态类型。Types statically approximate the runtime behaviour of an expression.
意思就是static type checking
本身是在程序运行之前进行类型检查——通过syntatic structure of expressions
。它可以保证通过静态类型检查的程序在运行时一定不会core dump
。这样的statically typed PL.
比如Ocaml/Java/ML/Haskell/Scala等
也被称为是type-safe
的。
Expressions, Names, Values and Types
在函数式中没有语句statements
,最基本的expression
就是numbers/strings/booleans
。
1 | # 2 + 3;; |
上面代码中的格式为<name> : <type> = <value>
。我们将;;
之前的式子称为expressions
,将整个计算过程称为evaluation
,而将计算的结果称为values
。
注意在Ocaml
中基础运算符并没有被重载,故浮点运算都是在后面有一个,
。这主要是因为Ocaml
是静态类型的,不会在运行时去根据运算符参数来推导重载的operators
。
通过类型检查的程序,其evaluation
要么更新到一个新的state
要么报一个built-in runtime error
。比如:
1 | # 3 / 0;; |
Variables, Bindings and Functions
Ocaml
是一个call-by-value
的语言——it binds values to variable names not expressions
。这里有一个binding
的概念,区分于assignment
。我们只是在values
和name
之间建立了“链接”——并没有新的state
被创建——我理解和imperative PL.
不同的应该是内存模型比如C
中对局部变量是放在栈上的但是确实创建了内存。当然这种“链接”也是可以更新的。在Ocaml
中用let
表达式来建立一个绑定。
1 | # let x = 3 * 3;; |
变量的作用域是根据binding stack
来确定的。局部或者临时创建的binding
都会被push
进binding stack
中,在不需要的时候会被GC
清除。
1 | # let k = 4;; |
这里有一个新的let <name> = <expression 1> in <expression 2>
结构——将expression1
的value
绑定到name
上,然后继续使用这个新的绑定对expression 2
进行evalutions
。
在函数式中let
不仅可以绑定基本类型,还可以绑定函数。
1 | let area : float -> float = function r -> pi *. r *. r;; |
上面的三种写法是等价的,都得到函数类型val area : float -> float = <fun>
。有意思的是函数作为一种绑定,也是在binding stack
中的,所以它只能看到过去的绑定:
1 | # let (pi*float) = 3.0 |
最后介绍一下递归函数。在Ocaml
中递归函数使用let rec
来声明。比如定义factorial
:
1 | # exception Domain;; |
在函数式中所有的函数都可以被写成尾递归的形式然后进一步优化——这是因为尾递归函数的栈帧可以被优化掉。比如:
1 | # let fact_tr n = |
Data Types and Pattern Matching
这一节可以在Ocaml
中定义自己的递归/非递归的数据类型。
Ocaml
使用type
来定义有限无序的非递归集合,这里举一个扑克牌的例子:
1 | # type suit = Clubs | Spades | Hearts | Diamonds;; |
Ocaml
使用pattern matching
模式匹配来操作这些数据结构。这里有两个概念——patterns
和constructors
。每一个pattern
由不同的constructors
填充——这里的构造器指的是每一个和类型元素同名的构造器比如Clubs
。Ocaml
规定构造器首字母必须大写,且匹配是从上到下的顺序。
进一步的,为了简化代码而支持inductively
的deep pattern
构造形式如下:
1 | # let rec add c1 c2 = match c1 c2 with |
这里会发现构造了一个比较复杂的类型('a -> (suit*int)*(suit*int)) -> 'a -> int
。其中type1 * type2
为一个pair
,而`a
为任意类型。
上述函数的基本类型推导过程为两个任意类型为输入参数,int
为输出。其中任意类型为(suit*int)*(suit*int)
的。
上面扑克牌的例子中hand
定义一组无限递归集合。在hand
类型中,Empty
和Hand
分别为两个constructors
。Empty
没有输入参数,而Hand
以a tuple of a card and another hand
的一个tuple
为输入参数。看下面的实例化例子:
1 | # let hand0: hand = Empty |
这个hand
类型实际上就是一棵退化的二叉树(只能靠右生长)——叶子是Empty
, 而Hand
构造器使用两个参数来建树。
下面我们定义一个函数,这里函数的类型是suit -> hand -> hand
,功能是给定一个suit -> hand
的输入,然后从hand
中找有匹配到suit
类型的hand
然后返回。
1 | # let rec extract (s:suit) (h:hand) = match h with |
上一个函数中的Empty
实际上就是一个终结符,意味着如果hand
类型都不匹配的话就直接返回Empty
。但是如果我要匹配一个card
的话,没有匹配上该怎么返回呢?Ocaml
引入一个optional data type
。比如下面的函数:
1 | # type 'a option = None | Some of 'a |
其中option type
表示由Some
构造器实现的类型为`a
的元素集合,是符合多态的任意类型。