【转】OCaml基础知识
出自:http://www.nirvanastudio.org/ocaml/the-basics-of-ocaml.html
注释
OCaml的注释是用(*
and *)
来分隔的,如下:
(* 这是一个单行注释 *)
(* 这是一个
* 多行
* 注释
*)
换句话说,注释的方式和原始的C(/* ... */
)一样。
目前还没有单行注释的语法(就是类似Perl的# ...
或者C99/C++/Java的// ...
)。是否使用##...
还没有确定,而且我极力推荐OCaml的人以后能将其加入到语言中。
OCaml可以处理嵌套的(* ... *)
,这可以让你很方便地注释某个代码区域:
(* 这段代码坏的……
(* 质数测试. *)
let is_prime n =
(* 对自己说:在邮件列表上问一下 *) XXX;;
*)
函数调用
假设你已经写了一个函数——叫它repeated
吧——,它需要一个字符串s
和一个数字n
作为参数,并返回一个新的字符串,包含了s
重复了n
次的结果。
在大部分C派生的语言中,调用这个函数类似于:
repeated ("hello", 3) /* C代码 */
这表示“使用两个参数调用函数repeated
,第一个参数是字符串hello,第二个参数是数字3”。
OCaml,和其他函数式语言一样,在函数调用的写法和括号的用法是有所不同的,所以容易产生很多错误。在OCaml中同样的函数调用是:
repeated "hello" 3 (* OCaml代码 *)
注意——不需要括号,参数之间也不需要逗号。
那么,repeated ("hello", 3)
在OCaml中则有另外一番深刻的含义。它表示“使用一个参数调用函数repeated
,该参数为包含两个元素的一个‘pair’(偶对)结构”。这当然是错误的,因为repeated
函数需要两个参数,而非一个,同时在任何情况下第一个参数都应该是一个字符串,而非一个偶对。不过目前还不用管偶对(元组“tuple”)只要记住在函数调用的参数两边加上括号和中间加上逗号是错误的。
再看另外一个函数——get_string_from_user
——它输入一个提示字符串并返回由用户输入的字符串。我们希望将这个字符串传入repeated
。下面是C和OCaml的版本:
/* C代码: */
repeated (get_string_from_user ("请输入一个字符串。"), 3)
(* OCaml代码: *)
repeated (get_string_from_user "请输入一个字符串。") 3
仔细看一下括号和逗号的用法。一般规则是:“括号要放在整个函数调用两边——不要将括号放在函数调用的参数周围”。下面还有一些例子:
f 5 (g "hello") 3 (* f有三个参数,g有一个参数 *)
f (g 3 4) (* f有一个参数,g有两个参数 *)
# repeated ("hello", 3);; (* OCaml 将会指出这个错误 *)
This expression has type string * int but is here used with type string
定义一个函数
你已经知道如果在其他主流语言中如何定义函数(对熟悉Java的来说,静态方法)。那么在OCaml要怎么做呢?
OCaml的语法十分简练,令人感觉愉悦。下面是一个输入两个浮点数并计算平均数的函数:
let average a b =
(a +. b) /. 2.0;;
将这段内容输入OCaml“顶层”(在Unix上,从外壳中输入ocaml
),然后就应该看到如下内容:
# let average a b =
(a +. b) /. 2.0;;
val average : float -> float -> float = <fun>
如果你仔细看函数定义,然后看看OCaml给你输出的内容,你就会产生一系列疑问:
- 代码中额外的句号是干什么用的?
float -> float -> float
这串东西是什么意思?
在下一节中,我会来回答这些问题,不过首先我要继续在C中定义同样的函数(Java中的定义应该会和C十分相似),而且这也很可能会引发更多的疑问。下面是average
的C版本:
double
average (double a, double b)
{
return (a + b) / 2;
}
现在再回头看一下简短得多的OCaml定义。你很有可能会问:
- 为什么在OCaml的版本中并没有定义
a
和b
的类型?OCaml是如何知道它们是何种类型的(更进一步问,OCaml是否知道它们的类型,或者OCaml完全就是动态类型的)? - 在C中,
2
会隐式转换成一个double
,但是为何OCaml不能做同样的事情? - OCaml写
return
的方式是什么?
好吧,现在就来回答其中一些问题。
- OCaml是一个静态强类型语言(换句话说,类型上没有任何像Perl中出现的那种动态的东西)。
- OCaml使用类型推断来计算出类型,所以无需进行声明。如果像上面那样在OCaml顶层输入代码,OCaml会告诉你(它所认为的)你的函数的正确类型。
- OCaml不会进行任何隐含转换。如果要一个浮点数,必须写作
2.0
,因为2
是一个整数。 - 因为OCaml不允许操作符重载,所以用不同的操作符来表示“两个整数相加”(
+
)和“两个浮点数相加”(+.
注意后面的点),其他算术操作符也类似。 - OCaml会返回函数中最后一个表达式,所以无需像C那样写
return
。
真正的细节在下面。
基本类型
OCaml中的基本类型有:
OCaml 类型 范围
int 32位处理器上是31位有符号整数(大约在+/- 10亿之间),
或者在64位处理器上是63位有符号整数
float IEEE 双精度浮点数,等同于C的double
bool 一个布尔型,true或false
char 一个8位字符
string 一个字符串
unit 写作 ()
OCaml内部保留了一个int
中的一位是为了能自动进行内存使用的管理(垃圾收集)。这就是为什么基本的int
是31位而非32位(所以,如果使用64位平台,int
是63位)。实际上除了一些特殊的场合,这并不成为一个问题。例如,如果你是对循环进行计数,OCaml会限制数到10亿而非20亿。这也不会成为一个问题,因为如果在任何语言中,要使用到接近这个上限的数字,你就应该使用大数字bignum(OCaml中的Nat
和Big_int
模块)。不过,如果你确实需要32位类型来处理一些事情的话(eg.写加密代码或者网络栈相关的),OCaml还提供了一个nativeint
类型,匹配了你的平台上本地的整型。
OCaml没有基本的无符号整数类型,但是你可以使用nativeint
来达到相同的效果。目前我只能说OCaml完全没有单精度浮点数的支持。
OCaml提供了一个用于字符的char
类型,例如,写作'x'
。不幸的是char
类型并不能支持Unicode或者UTF-8。这在OCaml中是一个很严重的瑕疵,应该被修正,但是现在可以使用comprehensive Unicode libraries来解决这个问题。
字符串不仅仅是字符的列表。它们有自己更加有效的内部表示方式。
unit
类型是一种类似于C中void
的东西,but we'll talk about it more below.
隐式转换对比显式转换
在C派生的语言中int在某些特定的环境下会自动提升为浮点数。例如,如果写1 + 2.5
,那么第一个参数(是一个整数)会提升为一个浮点数,结果也同样是一个浮点数。就好像你写了((double) 1) + 2.5
,不过都是隐含完成的。
OCaml则从不像这样进行隐式转换。在OCaml中,1 + 2.5
则是一个类型错误。OCaml中的+
操作符要求两个整数作为参数,而这里我们给出了一个整数和一个浮点数,所以它会报告这样的错误:
# 1 + 2.5;;
^^^
This expression has type float but is here used with type int
要将两个浮点数相加,你则需要另外一个不同的操作符,+.
(注意后面的点)。
OCaml不会自动将整数提升为浮点数,所以这也是一个错误:
# 1 +. 2.5;;
^
This expression has type int but is here used with type float
这里OCaml在抗议第一个参数。
如果你确实需要将一个整数和一个浮点数相加,要怎么做?(假设它们存储在叫做i
和f
)。在OCaml中你需要显式转换:
float_of_int i +. f;;
例子:正确的方法# float_of_int 10+.15.5;;
- : float = 25.5
错误:# 10+.5.5;;系统提示信息:Characters 0-2:
10+.5.5;;
^^
This expression has type int but is here used with type float
#
float_of_int
是一个输入了一个int
并返回一个float
。除此之外还有很多此类函数,名字诸如int_of_float
、char_of_int
、int_of_char
、string_of_int
等等,同时功能基本和名字吻合。
由于将int
转换为float
是一个特别常用的操作,float_of_int
函数有一个较短的别名:以上例子可以简单地写为:
float i +. f;;
(注意和C不一样,一个类型和一个函数有同样的名称在OCaml中这是完全有效的。)
隐式转换好,还是显式转换好?
你可能会想这些显式转换很丑陋,甚至很耗费时间,。首先,OCaml需要显式转换 才能进行类型推断(见下文),同时类型推断又是一个奇妙的、节省时间的特点,可以很方便地抵消显式转换所带来的额外的键盘输入,如果你以前花了很长时间来 调试C程序的话,你就会知道(a)隐式的类型转换所造成的错误很难被发现,(b)你常常会花很多时间来计算在哪里要发生隐式转换。让类型转换明确化就可以 帮助进行调试。第三,某些转换(尤其是 int <-> float)实际上是十分昂贵的操作,所以你应该自己来做而不是隐藏。
普通的函数和递归函数
和C派生的语言不同,除非你明确使用let rec
替代let
来声明一个函数,否则这个函数就不能是递归的。下面是一个递归函数的例子:
let rec range a b =
if a > b then []
else a :: range (a+1) b
;;
注意range
调用了其自身。
let
和 let rec
之间唯一的区别就在于函数名的范围。如果上面的函数仅仅是用let
来定义的话,当调用range
时就会尝试调用一个已经存在的(前面定义过的)叫做range
的函数,而不是当前被定义的函数。使用let
和使用let rec
定义的函数之间没有任何性能上的差别,所以如果你愿意,你可以总是使用let rec
形式来进行定义,就可以获得类似于C语言的语义。
函数的类型
因为类型推断的存在,你可能很少甚至从不需要明确写出函数的类型。不过,OCaml常常会输出它所认为的函数的类型,所以你需要知道它的语法。对于一个带有参数arg1
和arg2
、……的函数,编译器会输出:
f : arg1 -> arg2 -> ... -> argn -> rettype
箭头语法现在看起来很奇怪,不过之后当我们接触所谓的“currying”,你就会明白为何要选它。现在我就要给出几个例子。
我的函数repeated
输入一个字符串和一个整数并返回一个字符串的类型为:
repeated : string -> int -> string
我们的函数average
输入两个浮点数并返回一个浮点数的,类型为:
average : float -> float -> float
OCaml标准的int_of_char
类型转换函数:
int_of_char : char -> int
如果一个函数什么也不返回(对C和Java程序员来说应该是void
),那么我们写为返回unit
类型。例如,下面是OCaml中等同于fputc
的是:
output_char : out_channel -> char -> unit
多态函数
现在对于某些东西有些奇怪。如何才能使一个函数可以输入任何类型作为其参数呢?下面是一个输入一个参数的奇怪函数,但是忽略了这个参数,仅返回3:
let give_me_a_three x = 3;;
那么这个函数的类型是什么呢?在OCaml中我们使用一个特殊的占位符来表示“任何你能想到的类型”。这是一个单引号跟着一个字母。前面的函数的类型一般会写作:
give_me_a_three : 'a -> int
其中'a
实际上就是表示任何类型。例如,你可以这样调用: give_me_a_three "foo"
或者give_me_a_three 2.0
,两者在OCaml中都是有效的表达式。
多态函数到底能有什么用现在你可能还不清楚,不过它们确实十分有用也十分常见,所以我们将稍后讨论它们。(提示:多态时一种类似于C++或Java 1.5中的模版和泛型的东西)。
类型推断
那么本教程的主题是函数式变成有很多十分神奇的特点, 同时OCaml则是包含了所有这些神奇特点的一门语言,这就使其对于真正的程序员的使用来说是一们非常实用的语言。但是奇怪的是这些大部分神奇的特点和 “函数式编程”都毫无关系。实际上,我已经讲述了第一个神奇特点,而且我还未讲解为何函数式变成要叫做“函数式”的。不管怎样,下面是第一个神奇特点:类 型推断。
只要记住:你不需要声明你的函数和变量的类型,因为OCaml会为你计算出来
另外OCaml会继续检查所有的类型匹配(甚至在不同的文件之间)。
但是OCaml也是一个实用的语言,因此它在类型系统中包含了一些后门可以让你在特殊的必要场合中绕过它。一般只有大牛们可能会绕过类型检验。
让我们回到前面我们输入到OCaml顶层的函数average
:
# let average a b =
(a +. b) /. 2.0;;
val average : float -> float -> float = <fun>
说来奇怪!OCaml能自己计算出该函数输入两个float
参数并输出一个float
。
它是如何做到的呢?首先它看在哪里用到了a
和b
,即在表达式(a +. b)
中。现在,+.
则是一个总是输入两个float
参数,所以通过简单的探测,a
和b
一定都是float
类型的。
第二,/.
函数返回一个float
,同时它也是average
的返回值,所以,average
必然返回一个float
。最后结果就是average
会拥有以下类型签名:
average : float -> float -> float
类型推断明显对于这类短小的程序很方便,不过甚至对于大型程序,都很有效,同时它是个重要的省时特点,因为它可以避免其他语言中的一大类错误,如段错误(segfault)、的NullPointerException
和ClassCastException
(或者是一些重要的但常常被忽略的运行时警告,如Perl)。