Nirvana Studio » 2006 » 九月 :: 分享知识,传播技术

Archive for 九月, 2006

OCaml模块

Posted by ShiningRay on 30th 九月 2006

原文地址:http://www.ocaml-tutorial.org/modules,翻译:ShiningRay

模块(Module)是什么?我们首先看一下在OCaml内建的一些模块,比如可以先看一下List模块。首先让解释器将List模块的签名打印出来。这里有一个小技巧,我们可以将其复制一份自己的:

module MyList = List;;

然后就会打印出:

module MyList :
   sig
   val length : 'a list -> int
   val hd : 'a list -> 'a
   val tl : 'a list -> 'a list
   val nth : 'a list -> int -> 'a
   val rev : 'a list -> 'a list
   val append : 'a list -> 'a list -> 'a list
   val rev_append : 'a list -> 'a list -> 'a list
   val concat : 'a list list -> 'a list
   val flatten : 'a list list -> 'a list
   val iter : ('a -> unit) -> 'a list -> unit
   val map : ('a -> 'b) -> 'a list -> 'b list
   val rev_map : ('a -> 'b) -> 'a list -> 'b list
   val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
   val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b
   val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
   val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
   val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
   val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a
   val fold_right2 :
     ('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c
   val for_all : ('a -> bool) -> 'a list -> bool
   val exists : ('a -> bool) -> 'a list -> bool
   val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
   val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
   val mem : 'a -> 'a list -> bool
   val memq : 'a -> 'a list -> bool
   val find : ('a -> bool) -> 'a list -> 'a
   val filter : ('a -> bool) -> 'a list -> 'a list
   val find_all : ('a -> bool) -> 'a list -> 'a list
   val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
   val assoc : 'a -> ('a * 'b) list -> 'b
   val assq : 'a -> ('a * 'b) list -> 'b
   val mem_assoc : 'a -> ('a * 'b) list -> bool
   val mem_assq : 'a -> ('a * 'b) list -> bool
   val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
   val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list
   val split : ('a * 'b) list -> 'a list * 'b list
   val combine : 'a list -> 'b list -> ('a * 'b) list
   val sort : ('a -> 'a -> int) -> 'a list -> 'a list
   val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list
   val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list
   val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
 end

这个小技巧告诉了我们List模块所有可以用的东西。

让我们假设虽然我们要利用List模块,但是出于某种原因,我们要排除某些功能并加入一些其它功能。我们可以制作一个自己的List模块的版本。下面的例子去掉了“val length…”函数。首先要注意“include List”。这个语句将会把List模块的定义给我们拉进来。重新将length函数定义为抛出一个异常,只要加两行代码就行了。

module MyList =
	struct
           include List 
           exception NO_LENGTH_ALLOWED
           let length l = raise NO_LENGTH_ALLOWED
       end;;

在下面一个例子中,我们就加入一个返回列表中任意一个元素的函数。我们还需要用到length函数,同时可能对于列表中没有任何元素的情况,还要加入另一个异常。

module MyList =
       struct
           include List 
           exception EMPTY_LIST
           let rec rand_element l = 
               if l = [] then raise EMPTY_LIST
               else 
                   let len = float_of_int(List.length l) in
                   let r = Random.float 1.0 in
                   let e = int_of_float(len *. r) in
                   List.nth l e;;
       end;;

Posted in OCaml | No Comments »

OCaml函数式编程

Posted by ShiningRay on 23rd 九月 2006

什么是函数式编程

我们已经在本教程中学了有一段时间了,然而我们还没有真正考虑过函数式编程(functional programming)。目前文中讲述的一系列特点——丰富的数据类型、模式匹配、类型推断、嵌套函数——这些你只能想像存在于某种“超级C”语言中。有了这些神奇特点之后,就能让你的代码更精炼、更容易阅读并且错误更少,但但实际上它们和函数式编程的关系却不是很大。事实上我要说的观点是函数式语言如此神奇并非是因为其函数式编程,而是因为我们长时间局限于类C语言之中,而同时编程的界限却在不断地继续扩展。所以当我们在第N次重复写struct { int type; union { ... } }的时候,ML和Haskell程序员已经可以对数据类型使用安全变体和模式匹配了。当我们还在小心翼翼地free每个我们所malloc的东西时,自从80年代就已经有能比手工编码更好的垃圾收集器了。

好吧,说完这些之后,我就可以告诉你什么是函数式编程了。

最基本的定义为(不过不是很让人明白的):在函数式语言中,函数是第一等公民。

光说不练,难以令人理解,所以先看一个例子:

# let double x =
    x * 2
  in
  List.map double [ 1; 2; 3 ];;
- : int list = [2; 4; 6]

在这个例子中,我首先定义了一个叫做double嵌套函数,输入一个参数x并返回x * 2map对给定的列表([1; 2; 3])中的每个元素调用double,产生了这个结果:一个新的列表,其中每个数字都变成了原来的两倍。

我们已经知道map是一个高阶函数(higher-order function,简称HOF)。高阶函数就是说这个函数可以用另一个函数作为它的一个参数,听起来很深奥的说法。

目前来说这个概念还很简单。如果你熟悉C/C++,就会觉得它看上去好像就是传递一个函数指针。Java则有种叫做匿名类的讨厌东西,类似于一种迟钝的、冗长的闭包。如果你了解Perl,那么你可能已经知道并正在使用Perl的闭包以及Perl的map函数,它就是我们正在讨论的东西。实际上Perl也是一个很好的函数式语言。

闭包(Closure)指携带着它们被定义所处的“环境”的函数。特别地说,一个闭包可以引用在它定义时可用的那些变量。下面我们来泛化前面的函数,使得函数可以输入任何整数列表并将每个元素乘以一个任意值n

let multiply n list =
  let f x =
    n * x
  in
  List.map f list
  ;;

因此:

# multiply 2 [1; 2; 3];;
- : int list = [2; 4; 6]
# multiply 5 [1; 2; 3];;
- : int list = [5; 10; 15]

multiply中需要注意的重点是嵌套函数f。这是一个闭包。看一下f是如何使用n的值的,它并未实际作为一个明确的传递给ff而是直接从它的环境中获取了n的值——它是multiply函数的参数,因此在其中是可用的。

听起来可能很直观,不过让我们再仔细看一下对映射的调用:List.map f list

map是定义在List模块中的,和当前的代码并不在一起。换句话说,我们将f传递到一个“很久很久以前,在一个遥远的星系中”定义的模块。就我们所知而言,代码可以将f传递到其他模块,或者将f的引用保存在某处等待以后调用。无论怎么做,闭包都会确保f一定可以访问其来源的环境,并获取n

下面是从lablgtk中截取的实际的例子。它其实是一个类的方法(我们还没讨论过类和方法,现在只要将其认为是一个函数定义就行了)。

class html_skel obj = object (self)
  ...
  ...
  method save_to_channel chan =
    let receiver_fn content =
      output_string chan content;
      true
    in
    save obj receiver_fn

首先,你要知道在这个方法最后调用的save函数,其第二个参数是一个函数(receiver_fn)。它再重复调用receiver_fn,将来自部件的文本片断储存起来。

现在看一下recevier_fn的定义。这个函数正是一个闭包因为它保存了一个从其环境中引入的chan的引用。

部分函数的应用以及柯里化

让我们定义一个将两数相加的加法函数:

let plus a b =
  a + b
  ;;

下面给在教室后面睡觉的家伙们一些问题:

  1. plus是什么?
  2. plus 2 3是什么?
  3. plus 2是什么?

问题 1 很简单。plus是一个函数,它接受两个整数参数并返回一个整数。类型是这么写的:

plus : int -> int -> int

问题 2 就更简单了。plus 2 3是一个数字,整数5。类型是这么写的:

5 : int

但问题 3 怎么回答呢?貌似plus 2是一个错误。然而,实际上,并非如此。如果我们在OCaml的顶层输入这个表达式,那么它会告诉我们:

# plus 2;;
- : int -> int = <fun>

这不是一个错误。它告诉我们plus 2实际上是一个函数,它接受一个int并返回一个int。这是怎样的一个函数呢?首先我们给这个函数起个名字叫做(f),然后再进行一些实验,给它一些整数看看会发生什么:

# let f = plus 2;;
val f : int -> int = <fun>
# f 10;;
- : int = 12
# f 15;;
- : int = 17
# f 99;;
- : int = 101

在工程中,我们有足够的证据plus 2就是将2加上另一个数的函数。

回到原来的定义,让我们“填入”第一个参数(a)2,则获得了:

let plus 2 b =       (* 这不是真正的OCaml代码! *)
  2 + b
  ;;

我希望你可以了解为何plus 2就是一个函数。

看一下这些表达式的类型也许就能够一窥函数类型中用到的奇怪的->箭头记号的原理:

    plus : int -> int -> int
  plus 2 : int -> int
plus 2 3 : int

这个过程称之为currying(柯里化,也可能叫做uncurrying反柯里化,我至今也搞不清哪个是哪个)。它之所以如此命名是为了纪念Haskell Curry,他对lambda算子做出了一些相关的重要贡献。由于我要尽可能回避在OCaml背后的数学原理——因为这十分冗长也不切主题——所以这个问题上我就不深入了。如果你对此感兴趣,你可以通过Google搜索来查找更多关于柯里化的更多信息。

还记得我们前面的doublemultiply函数吗?multiply的定义是这样的:

let multiply n list =
  let f x =
    n * x
  in
  List.map f list
  ;;

现在我们来定义doubletriple,这些函数很好写,如下:

let double = multiply 2;;
let triple = multiply 3;;

它们其实都是函数,看:

# double [1; 2; 3];;
- : int list = [2; 4; 6]
# triple [1; 2; 3];;
- : int list = [3; 6; 9]

你也可以像这样直接使用部分应用(无需中间的f函数)

# let multiply n = List.map (( * ) n);;
val multiply : int -> int list -> int list = <fun>
# let double = multiply 2;;
val double : int list -> int list = <fun>

# let triple = multiply 3;;
val triple : int list -> int list = <fun>
# double [1; 2; 3];;
- : int list = [2; 4; 6]
# triple [1; 2; 3];;
- : int list = [3; 6; 9]

在上面的例子中,(( * ) n)( * )(乘法)函数的部分应用。注意空格是必须的,这样OCaml才不会认为(*是一个注释的开始。

你可以将中缀操作符放入括号中来定义函数。下面是定义的和前面的plus函数完全一样:

# let plus = (+);;
val plus : int -> int -> int = <fun>
# plus 2 3;;
- : int = 5

下面还有一些有趣的柯理化:

# List.map (plus 2) [1; 2; 3];;
- : int list = [3; 4; 5]
# let list_of_functions = List.map plus [1; 2; 3];;
val list_of_functions : (int -> int) list = [<fun>; <fun>; <fun>]

函数式编程的优势是什么?

函数式编程,和其他的优秀编程技术一样,它也是你的锦囊中用于解决某几类问题的有效工具。对于设计回调函数——它用多种用户,从GUI到事件驱动的循环——十分方便。对于表达泛型算法也十分好用。List.map就是一个泛型算法,用于将函数应用于任何类型的列表。类似的,你还可以定义处理树的泛型函数。某几种类型的数字问题也可以利用函数式编程更快地解决(例如,用数字计算数学函数派生出来的东西)。

纯粹的和不纯粹的函数式编程

一个纯粹的函数是指没有任何副作用的函数。副作用其实是指函数在其内部保存某种隐藏的状态。C中,strlen是一个纯函数的很好的例子。如果你对同样的字符串多次调用strlen,它总是返回同样的长度值。strlen的输出(长度)仅由其输入(字符串)决定,不依赖于任何其他东西。但不幸的是,C中很多函数都是不纯的。例如,malloc——如果你用同样的数字对其进行调用,它肯定都不会返回同样的指针给你。malloc当然还依赖于很多隐藏的内部状态(堆上分配的对象、使用的分配方法、从操作系统抓取页,等等)。

ML派生的语言,如OCaml,是“近乎纯粹的”。它们允许通过像引用和数组这类的东西产生一些副作用,但是大部分你写出来的代码都会是纯函数式的因为它们鼓励这种思考方式。Haskell——另一种函数式语言——则是完全纯函数式的。因为写不纯的函数有时候更加有用和有效,所以OCaml更加实用的。

使用纯函数还有一些理论上的好处。其中一个好处是,如果某个函数是纯粹的,那么如果使用同样的参数对其调用多次的话,编译器就只需要调用该函数一次。在C中有一个很好的例子:

for (i = 0; i < strlen (s); ++i)
  {
    // 做一些不影响s的事情
  }

如果就这样编译了,那么这个循环是O(n2)因为每次都要调用strlen (s)然后strlen又需要迭代整个s。如果编译器足够聪明可以推断出strlen是一个纯函数同时s又没有在循环中更新过,那么它就可以删除冗余的strlen的调用,就能使函数变为O(n)复杂度。那么编译器真的能这么做吗?在strlen的情况下,可以,而在其他情况下,可能就不行了。

集中于写短小的纯函数可以让你使用一种自地向上的方式来构建可复用的代码,同时边继续边测试每个小函数。而当前时尚的方式是使用一种自顶向下的方式来仔细设计你的程序,不过在作者的经历中,这往往会导致项目失败。

严格性和惰性

C派生的和ML派生的语言都是严格的。Haskell和Miranda则是非严格的,或者说是惰性的。OCaml默认是严格的,不过当必须时也可以进行惰性风格的编程。

在严格语言中,给函数传递参数的时候都是先计算好的,然后把结果传递给函数。例如,在严格语言中,下面的调用肯定会导致“被零除”的错误:

give_me_a_three (1/0);;

如果你用一些常见的语言编程的话,这就是其工作的方式,如果它能以任何方式运行起来的话,你一定十分惊讶。

在一个惰性语言中,就会有一些其他奇怪的事情发生。传递给函数的参数只有当函数实际用到它们的时候才会进行计算。还记得前面give_me_a_three函数会抛弃它的参数,总是返回3么?在惰性语言中,上面的调用并不会失败,因为give_me_a_three从不会看它第一个参数,所以第一个参数并不会被计算,所以被零除并不会发生。

惰性语言还能让你做一些很古怪的事情,比如定义一个无限长的列表。前提是你不会真的去迭代整个列表,这就没问题(换句话说,比如你只要获取前10个元素)。

OCaml是一个严格语言,不过它有一个Lazy模块可以让你写一些惰性表达式。下面有一个例子。首先我们给1/0创建一个惰性表达式:

# let lazy_expr = lazy (1/0);;
val lazy_expr : int lazy_t = <lazy>

注意这种惰性表达式的类型是int lazy_t

因为give_me_a_three接受'a(任何类型)作为参数,所以我们可以将惰性表达式传递给该函数:

# give_me_a_three lazy_expr;;
- : int = 3

如果要计算一个惰性表达式,必须使用Lazy.force函数:

# Lazy.force lazy_expr;;
Exception: Division_by_zero.

装箱类型和拆箱类型

当讨论函数式语言的时候,有一个术语你会经常听到,这就是“装箱”。当我第一次听到这个术语的时候,我也很困惑,其实如果你以前用过C/C++或者Java,那么对你来说装箱类型和拆箱类型之间的区别是相当简单的(在Perl中,一切东西都是被装箱过的)。

如何去思考一个被装箱的对象呢,在C中这种对象就是使用malloc在堆上分配的(或者在C++中等同于new),同时/或者通过一个指针来引用的对象。看一下这个C程序的例子:

#include <stdio.h>
 
void
printit (int *ptr)
{
  printf ("the number is %d\n", *ptr);
}
 
void
main ()
{
  int a = 3;
  int *p = &a;
 
  printit (p);
}

变量a是分配在栈上的,所以肯定是被拆箱过的。

函数printit接受一个装箱过的整数并将其打印出来。

下面的图像是了一组拆箱(上部)和装箱(下部)整数之间的对比:

boxedarray.png

拆箱了的整数要比装箱了的快很多。另外,因为单独分配更少,所以对于拆箱的对象的类型垃圾收集更加快速也更加简单。

在C/C++中,你应该对构造以上两种类型的数组是没有问题的。在Java中,有两种类型,int是拆箱型的,而Integer是装箱型的,因此相对不如前者有效。在OCaml中,所有的基本类型都是拆箱型的。

Posted in OCaml | 2 Comments »

FireBug 控制台函数说明

Posted by Nicholas Ding on 20th 九月 2006

原文地址:http://www.joehewitt.com/software/firebug/docs.php

FireBug 是一个非常实用的JavaScript以及DOM查看调试工具,是 Firefox 的一个插件。使用 FireBug 调试 AJAX 应用非常方便,终于可以告别 alert 时代了!

Console Logging 函数

FireBug 为所有 Web 页面提供了一个 console 对象。这个对象有以下函数:

Logging 基础

console.log(”message” [,objects]) - 将一个字符串打印到控制台。字符串可以包含任何“String Formatting”小节描述的模式。字符串后面的对象应该用来取代之前字符串中的模式。(译者注:大家用过C里面 printf 吧,效果基本是一样的。)

Logging 等级

通常根据不同的等级来区分Logging的严重程度是很有帮助的。FireBug 提供了4个等级。为了达到视觉分离的效果,这些函数与 log 不同的地方就是它们在被调用的时候会自动包含一个指向代码行数的链接。

console.debug(”message” [,objects]) - 记录一个 debug 消息。
console.info(”message” [,objects]) - 记录一个信息.
console.warn(”message” [,objects]) - 记录一个警告.
console.error(”message” [,objects]) - 记录一个错误.

断言

断言是一条确保代码规则的非常好的途径。console 对象包含了一系列各种类型的断言函数,并且允许你编写自己的断言函数。

console.assert(a, “message” [,objects]) - Asserts that an a is true.
console.assertEquals(a, b, “message” [,objects]) - Asserts that a is equal to b.
console.assertNotEquals(a, b, “message” [,objects]) - Asserts that a is not equal to b.
console.assertGreater(a, b, “message” [,objects]) - Asserts that a is greater than b.
console.assertNotGreater(a, b, “message” [,objects]) - Asserts that a is not greater than b.
console.assertLess(a, b, “message” [,objects]) - Asserts that a is less than b.
console.assertNotLess(a, b, “message” [,objects]) - Asserts that a is not less than b.
console.assertContains(a, b, “message” [,objects]) - Asserts that a is in the array b.
console.assertNotContains(a, b, “message” [,objects]) - Asserts that a is not in the array b.
console.assertTrue(a, “message” [,objects]) - Asserts that a is equal to true.
console.assertFalse(a, “message” [,objects]) - Asserts that a is equal to false.
console.assertNull(a, “message” [,objects]) - Asserts that a is equal to null.
console.assertNotNull(a, “message” [,objects]) - Asserts that a is not equal to null.
console.assertUndefined(a, “message” [,objects]) - Asserts that a is equal to undefined.
console.assertNotUndefined(a, “message” [,objects]) - Asserts that a is not equal to undefined.
console.assertInstanceOf(a, b, “message” [,objects]) - Asserts that a is an instance of type b.
console.assertNotInstanceOf(a, b, “message” [,objects]) - Asserts that a is not an instance of type b.
console.assertTypeOf(a, b, “message” [,objects]) - Asserts that the type of a is equal to the string b.
console.assertNotTypeOf(a, b, “message” [,objects]) - Asserts that the type of a is not equal to the string b.

测量(Measurement)

下面的一些函数可以让你方便的测量你的一些代码。

console.trace() - 记录执行点的堆栈信息。
console.time(”name”) - 根据 name 创建一个唯一的计时器。
console.timeEnd(”name”) - 根据 name 停止计时器,并且记录消耗的时间,以毫秒为单位。
console.count(”name”) - 记录该行代码执行的次数。

字符串格式化

所有 console 的 logging 函数都可以通过以下模式格式化字符串:

%s - 将对象格式化为字符串。
%d, %i, %l, %f - 将对象格式化为数字。
%o - 将对象格式化成一个指向 inspector 的超链接。
%1.o, %2.0, etc.. - 将对象格式化成包含自己属性的可交互的表格。
%.o - 将对象格式化成具有自身属性的一个数组。
%x - 将对象格式化成一个可交互的 XML 树形结构。
%1.x, %2.x, etc.. - 将对象格式化成一个可交互的 XML 数型结构,并且展开 n 层节点。

如果你需要一个真实的 % 符号,你可以通过一个转移符号就像这样 “\%”。

命令行函数

内建的命令行函数可以通过以下命令行使用:

$(”id”) - document.getElementById() 的简写。(译者注:跟 prototype.js 学来的吧?)
$$(”css”) - 返回一个符合 CSS 选择器的元素数组。
$x(”xpath”) - 返回一个符合 XPath 选择器的元素数组。
$0 - 返回最近被检查(inspected)的对象。
$1 - 返回最近被检查(inspected)的下一个对象。
$n(5) - 返回最近被检查的第n个对象。
inspect(object) - 将对象显示在 Inspector 中。
dir(object) - 返回一个对象的属性名数组。(译者注:跟 Python 学的?)
clear() - 清除控制台信息。

Posted in Ajax, JavaScript, Web?? | 7 Comments »

空指针、断言和警告

Posted by ShiningRay on 11th 九月 2006

原文地址:http://www.ocaml-tutorial.org/null_pointers,_asserts_and_warnings,翻译:ShiningRay

空指针

那么现在假设你在网站上放了个调查,询问读者的姓名和年龄。有一个问题是,如果由于某些原因一些读者不想给出他们的年龄——他们很顽固,拒绝填写这一栏。那么可怜的数据库管理员要怎么做呢?

假设年龄是由一个int来表示的,有两种方式来解决这个问题。最常见的方式(也是最有问题的)是当没有收集到年龄信息的时候,为年龄假设某种“特殊”的值。所以,假设当没有收集到数据时,年龄为-1,否则当有数据时,则填入年龄(即使它是无效的!)。它暂时完成了工作,直到你要开始对年龄进行计算,例如,计算网站访客的平均年龄。由于你忘了考虑这个特殊值,最后可能访客年龄的平均值为7(?),然后你就会雇佣设计师将长的单词去掉并到处使用简单的色块。

另一种方式,也就是正确的方式是将年龄存储在一个类型为“int或null”的字段中。下面是这个用于存储年龄的SQL表格:

CREATE TABLE users
(
  userid serial,
  name text NOT NULL,
  age int             -- 可以为空
);

如果没有收集到年龄数据,那么存到数据库中的是一个特殊的SQL NULL值。当你要计算平均值或其他东西的时候,SQL会自动忽略这些空值。

编程语言也支持空值,虽然某些语言中的使用要比其他一些简单。在Perl中,任何量(即数字或者字符串)都可以是undef(空在Perl中的说法)。这是导致很多警告的一个原因,但这些警告往往会被没有经验的程序员所忽略,即便这些警告可能表示严重的错误。在Java中,任何指向对象的引用都可以为空,所以在Java中可以将年龄存为Integer,就能让年龄的引用为null。在C语言中,指针当然能为空,但是如果要让一个简单的整数为空,首先要将其装箱为一个由malloc在堆上分配的对象。

OCaml则有其自己对空值问题的优雅解决方法,使用一个简单的多态变体类型,定义(在Pervasives中的)为:

type 'a option = None | Some of 'a

“空指针”写作None。上面的例子中的年龄的类型(一个可以为空的int)是int option(记住:它是类似于int listint binary_tree的逆向写法)。

# Some 3;;
- : int option = Some 3

可选int的列表如何:

# [ None; Some 3; Some 6; None ];;
- : int option list = [None; Some 3; Some 6; None]

And what about an optional list of ints?

# Some [1; 2; 3];;
- : int list option = Some [1; 2; 3]

断言、警告、致命错误以及stderr输出

Perl的一个很好的特点是有丰富的调试程序和处理异常错误的命令,包括打印栈轨迹、抛出和捕获异常等。OCaml没有这么丰富的调试命令——不过比Java要好一点,基本和C差不多,和Perl比就逊色一些。(在以后我们会更加详细地讨论异常。)

首先,assert输入一个表达式作为参数,并抛出一个异常。假设你没有捕获这个异常(捕获这个异常可能是不太明智的,尤其对于初学者来说),它会导致程序停止并输出发生错误的源文件和行号。例如:

# assert (Sys.os_type = "Win32");;
Exception: Assert_failure ("", 0, 30).

(当然,运行在Win32上是不会抛出错误的)

你也可以直接调用assert false来停止程序运行,不过你最好使用failwith

failwith "错误信息"会抛出一个Failure异常,我们再假设你没有捕获它,那么程序就会停止并给出错误信息。failwith常用于模式匹配中,像下面这个例子中:

match Sys.os_type with
    "Unix" | "Cygwin" ->   (* 代码省略 *)
  | "Win32" ->             (* 代码省略 *)
  | "MacOS" ->             (* 代码省略*)
  | _ -> failwith "this system is not supported"

注意在这个例子中有两个特别的模式匹配。一个是所谓的“范围模式”用于匹配"Unix"或者"Cygwin",还有一个是特殊的_模式,用于匹配“任何其他东西”。

如果你要调试程序的话,那么你就可能想要通过某种方式从函数中输出一个警告。下面有一个例子(注意红色高亮部分):

open Graphics;;
 
open_graph " 640x480";;
for i = 12 downto 1 do
  let radius = i * 20 in
  prerr_endline ("radius is " ^ (string_of_int radius));
  set_color (if (i mod 2) = 0 then red else yellow);
  fill_circle 320 240 radius
done;;
read_line ();;

如果你偏爱C风格的printf,那么可以尝试OCaml的Printf模块:

open Graphics;;
open Printf;;
 
open_graph " 640x480";;
for i = 12 downto 1 do
  let radius = i * 20 in
  eprintf "radius is %d\n" radius;
  set_color (if (i mod 2) = 0 then red else yellow);
  fill_circle 320 240 radius
done;;
read_line ();;

Posted in OCaml | 1 Comment »

数据类型和匹配

Posted by ShiningRay on 10th 九月 2006

原文地址:http://www.ocaml-tutorial.org/data_types_and_matching 翻译:ShiningRay

链表(Linked List)

和Perl一样,OCaml也将对列表的支持直接内建在语言中了。OCaml中一个列表的所有元素的类型必须一致。使用以下格式来写列表:

[1; 2; 3]

(注意是分号,不是逗号)。

[] 表示空列表。

一个列表有一个“”(第一个元素)和一个“”(剩下的元素)。头是一个元素,而尾则是一个列表,所以前面的例子中,表头是整数1,而表尾是list[2; 3]

另一种做法是使用cons操作符—— :: 。所以下面的列表写法是完全一样的:

[1; 2; 3]
1 :: [2; 3]
1 :: 2 :: [3]
1 :: 2 :: 3 :: []

为什么我要提到cons操作符呢?其实,当我们对列表使用模式匹配的时候,它是相当有用的,下面我说详细说明。

链表的类型

整数链表的类型是int list,一般来说,foo类型的链表的类型就是foo list

由此可以看出,链表的所有元素都必须是同一种类型的。但是类型却可以是多态的(即,'a list),当你要写操作"lists of anything"(任意类型的列表)的泛型函数时,这就相当有用了(注意,'a list 并不代表每个单独的元素可以有不同的类型——所以你还是不能使用它来构造包含混合类型的列表,也就是说,元素的类型可以任意,但所有元素的类型必须相同)。

length函数是定义在OCaml的List模块中的,它是一个很好的例子。它不论列表包含的整数还是字符串或者对象、 还是什么小怪物,List.length函数都可以对其进行处理。因此,List.length的类型是:

List.length : 'a list -> int

结构

C 和 C++都有一个简单的struct的概念,它是结构的缩写。Java中可以使用类来达到类似的效果,虽然费事得多。

考虑以下简单的C结构

struct pair_of_ints {
  int a, b;
};

在OCaml中,能等同于它的最简单的东西是tuple(元组),诸如(3, 4),其类型为int * int。与列表不同,元组可以包含不同类型的元素,所以例如(3, "hello", 'x')的类型为int * string * char

在OCaml中还有一种稍微复杂一点的写C结构的方法,是使用一个record(记录)。记录,和C结构一样,可以让你对其中的元素进行命名,而无需记住它们出现的顺序。下面是与上面的C结构等同的记录:

type pair_of_ints = { a : int; b : int };;

这就可以定义一个类型,那么如何实际创建这种类型的对象呢?

{ a=3; b=5 }

注意我们在类型定义中使用了":",而在创建这种类型的对象时使用了"="。

下面是一些例子:

# type pair_of_ints = { a : int; b : int };;
type pair_of_ints = { a : int; b : int; }
# {a=3; b=5};;
- : pair_of_ints = {a = 3; b = 5}
# {a=3};;
Some record field labels are undefined: b

所以OCaml不会允许在结构中有未定义的字段。

变体(条件联合和枚举)

C语言中并不直接存在“条件联合”("qualified union"),虽然gcc编译器中对它有一些支持。 下面是C中使用条件联合的一种模式:

struct foo {
  int type;
#define TYPE_INT 1
#define TYPE_PAIR_OF_INTS 2
#define TYPE_STRING 3
  union {
    int i;        // If type == TYPE_INT.
    int pair[2];  // If type == TYPE_PAIR_OF_INTS.
    char *str;    // If type == TYPE_STRING.
  } u;
};

大家应该可以明白,而且它看上去并不太好看。首先,它不安全:程序员可能会犯一个错误,并意外地(假设)当结构中实际存储着字符串的时候调用了u.i字段。同时编译器也不能很容易检查是否所有可能的类型都在switch语句中检验过了(你可以使用一个enum类型来解决这个特殊的问题)。程序员也可能忘记设置type字段,这就可能导致这种奇怪的事情发生。此外,这种模式也很麻烦。

下面是在OCaml中等同于上面的一种优雅并简练的解决方法:

type foo = Nothing | Int of int | Pair of int * int | String of string;;

这就是我们所需的类型定义。每个由| 分开的部分的第一个部分称做构造器(constructor)。你可以将其换作任何东西,只要其首字母大写。如果构造器可以用于定义一个值,那么在其后跟上of type部分,这里类型type总是以小写字母开头。在前面的例子中, Nothing被用作一个常量,其他的构造器则用于创建值。

要实际创建这种类型的东西,你可以输入:

Nothing
Int 3
Pair (4, 5)
String "hello"
     &c.

其中每个表达式的类型都为foo

注意当定义类型的时候,使用of,当写该类型的元素的时候则没有。

扩展一下,一个简单的Cenum的定义为:

enum sign { positive, zero, negative };

在OCaml中可以写作:

type sign = Positive | Zero | Negative;;

递归变体(用于树)

变体也可以是递归的,这也常用于定义树状结构。这就是函数式语言强大表达力之所在:

type binary_tree = Leaf of int | Tree of binary_tree * binary_tree;;

以下是一些二叉树。你可以在纸上画一下它们的结构练习一下。

Leaf 3
Tree (Leaf 3, Leaf 4)

Tree (Tree (Leaf 3, Leaf 4), Leaf 5)
Tree (Tree (Leaf 3, Leaf 4), Tree (Tree (Leaf 3, Leaf 4), Leaf 5))

参数化变体

上一节中的二叉树每个叶上都是整数,但如果我们只是要描述一个二叉树的形状,而留着以后再确切决定存储何种叶结点,该如何做?这时我们可以使用一个参数化(多态)变体,如下:

type 'a binary_tree = Leaf of 'a | Tree of 'a binary_tree * 'a binary_tree;;

这是一个通用类型。在每个叶上都存储了整数的类型称之为int binary_tree。类似的,存储字符串的特定类型称之为string binary_tree。在下面的例子中我们会在OCaml中输入一些实例并让类型推断系统将类型告诉我们:

# Leaf "hello";;
- : string binary_tree = Leaf "hello"
# Leaf 3.0;;
- : float binary_tree = Leaf 3.

注意类型的名称是反向的。将它类型名称和列表的类型名称进行一下类比,比如int list等等。

事实上'a list也这样“逆向”写并不是偶然。列表只不过是有着以下一些特殊定义的参数化变体类型:

type 'a list = [] | :: of 'a * 'a list   (* OCaml伪代码 *)

事实上,上面的定义是不能通过编译的。下面才是等同的有效定义:

# type 'a list = Nil | :: of 'a * 'a list;;
type 'a list = Nil | :: of 'a * 'a list
# Nil;;
- : 'a list = Nil
# 1 :: Nil;;
- : int list = :: (1, Nil)
# 1 :: 2 :: Nil;;
- : int list = :: (1, :: (2, Nil))

记得前面我们说过列表可以有两种写法,使用[1; 2; 3]这样的语法糖或者较为正式的1 :: 2 :: 3 :: []。如果你看懂了上面的'a list的定义,那么你就会明白正规定义的理由和机制了。

列表、结构和变体总结

OCaml名称     类型定义例子                使用例子

list            int list                               [1; 2; 3]
tuple           int * string                           (3, "hello")
record          type pair = { a : int; b : string }    { a = 3; b = "hello" }
variant         type foo = Int of int                  Int 3
                         | Pair of int * string
variant         type sign = Positive | Zero            Positive
                          | Negative                   Zero
parameterised   type 'a my_list = Empty                Cons (1, Cons (2, Empty))
  variant                  | Cons of 'a * 'a my_list

(数据类型的)模式匹配

接下来的函数式编程语言的一个神奇特点是能够解开数据结构并对数据进行模式匹配。这又不是一个真正的“函数式”的特点——你可能知道某些C的变种也可以这么做,不过肯定没有这么神奇。

我们现在一个实际的编程需求开始:我想要将一个简单的像n * (x + y)这样的数学表达式表示为将其中的乘号分开的形式,得到n * x + n * y

让我们定义满足这些表达式的一个类型:

type expr = Plus of expr * expr        (* 表示 a + b *)
          | Minus of expr * expr       (* 表示 a - b *)
          | Times of expr * expr       (* 表示 a * b *)
          | Divide of expr * expr      (* 表示 a / b *)
          | Value of string            (* "x", "y", "n", etc. *)
	  ;;

表达式n * (x + y)可以写作:

Times (Value "n", Plus (Value "x", Value "y"))

现在让我们写一个可以将Times (Value "n", Plus (Value "x", Value "y"))输出为类似n * (x + y)的东西的函数。实际上,我要写两个函数,一个用于将表达式转换成漂亮的字符串,另一个将其打印出来(这样做的原因是我可能也想将同样的字符串写入到一个文件,而我又不想为了这点小事就将整个函数再重写一遍——尽可能复用代码)。

let rec to_string e =
  match e with
    Plus (left, right)   -> "(" ^ (to_string left) ^ " + " ^ (to_string right) ^ ")"
  | Minus (left, right)  -> "(" ^ (to_string left) ^ " - " ^ (to_string right) ^ ")"
 
  | Times (left, right)  -> "(" ^ (to_string left) ^ " * " ^ (to_string right) ^ ")"
  | Divide (left, right) -> "(" ^ (to_string left) ^ " / " ^ (to_string right) ^ ")"
 
  | Value v -> v
  ;;
 
let print_expr e =
  print_endline (to_string e);;

(注意:^操作符用于联结字符串。)

立刻对其进行测试:

# print_expr (Times (Value "n", Plus (Value "x", Value "y")));;
(n * (x + y))

模式匹配的一般形式是:

match object with
  pattern    ->  result
| pattern    ->  result
    ...

左边的模式可以是简单的,如上面的to_string中所示,也可以是复杂的、嵌套的。下面一个例子是分解形式为n * (x + y)或者(x + y) * n的表达式的函数,为此我们将用到一个嵌套模式:

let rec multiply_out e =
  match e with
    Times (e1, Plus (e2, e3)) ->
      Plus (Times (multiply_out e1, multiply_out e2),
            Times (multiply_out e1, multiply_out e3))
  | Times (Plus (e1, e2), e3) ->
      Plus (Times (multiply_out e1, multiply_out e3),
            Times (multiply_out e2, multiply_out e3))
  | Plus (left, right) -> Plus (multiply_out left, multiply_out right)
  | Minus (left, right) -> Minus (multiply_out left, multiply_out right)
  | Times (left, right) -> Times (multiply_out left, multiply_out right)
  | Divide (left, right) -> Divide (multiply_out left, multiply_out right)
  | Value v -> Value v
  ;;

下面是结果:

# print_expr (multiply_out (Times (Value "n", Plus (Value "x", Value "y"))));;
((n * x) + (n * y))

multiply_out函数是如何运作的呢?第一个模式是匹配形如e1 * (e2 + e3)的表达式的Times (e1, Plus (e2, e3))。现在看一下这个模式的右边的东西,其实它就等同于(e1 * e2) + (e1 * e3)

第二个模式的工作基本类似,除了针对形如(e1 + e2) * e3的表达式。

剩下的模式并不会改变表达式的形式,但是它们也十分关键,它们递归对子表达式调用了multiply_out函数。这确保了表达式中的所有的子表达式都能得到处理(除非你只需要分解表达式的最顶层,如果这样,你只需要将这些余下的模式替换成简单的e -> e规则)。

我们是否能逆向进行(即提取公共的子表达式)?当然可以(不过要复杂一些)!下面的版本只针对顶层表达式。你一定可以对其进行扩展来完美地处理表达式的所有层次或者更加复杂的情况:

let factorize e =
  match e with
    Plus (Times (e1, e2), Times (e3, e4)) when e1 = e3 -> Times (e1, Plus (e2, e4))
  | Plus (Times (e1, e2), Times (e3, e4)) when e2 = e4 -> Times (Plus (e1, e3), e4)
  | e -> e
  ;;

# factorize (Plus (Times (Value "n", Value "x"), Times (Value "n", Value "y")));;
- : expr = Times (Value "n", Plus (Value "x", Value "y"))

上面的factorize函数引入了另外两个特点。你可以对每个模式匹配添加所谓的守卫(guard)。一个守卫(条件)是指跟在when的条件表达式,它表示该模式匹配只有在模式被匹配了并且满足了在when子句中的条件时,才会发生模式匹配。

match object with
  pattern    [ when condition ]   ->  result
  pattern    [ when condition ]   ->  result
    ...

第二个特点是用于测试两个表达式之间“结构相等”(structural equality)的=操作符。这表示会递归进入每个表达式检测它们是否在每一个级别上都完全一致。

OCaml可以在编译期检测出你是否已经在你的模式中将所有的可能性都涉及了。如果我给上面的type expr类型定义添加一个Product变体的话:

type expr = Plus of expr * expr        (* 表示a + b *)
          | Minus of expr * expr       (* 表示a - b *)
          | Times of expr * expr       (* 表示 a * b *)
          | Divide of expr * expr      (* 表示 a / b *)
          | Product of expr list       (* 表示 a * b * c * ... *)
          | Value of string            (* "x", "y", "n", etc. *)
	  ;;

然后我未对to_string函数加以修改就重新编译它,结果OCaml报告了以下警告:

Warning: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
Product _

Posted in OCaml | 1 Comment »

OCaml基础知识

Posted by ShiningRay on 9th 九月 2006

原文地址:http://www.ocaml-tutorial.org/the_basics 翻译:ShiningRay

注释

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的版本中并没有定义ab的类型?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中的NatBig_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在抗议第一个参数。

如果你确实需要将一个整数和一个浮点数相加,要怎么做?(假设它们存储在叫做if)。在OCaml中你需要显式转换:

float_of_int i +. f;;

float_of_int是一个输入了一个int并返回一个float。除此之外还有很多此类函数,名字诸如int_of_floatchar_of_intint_of_charstring_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调用了其自身。

letlet rec之间唯一的区别就在于函数名的范围。如果上面的函数仅仅是用let来定义的话,当调用range时就会尝试调用一个已经存在的(前面定义过的)叫做range的函数,而不是当前被定义的函数。使用let和使用let rec定义的函数之间没有任何性能上的差别,所以如果你愿意,你可以总是使用let rec形式来进行定义,就可以获得类似于C语言的语义。


函数的类型

因为类型推断的存在,你可能很少甚至从不需要明确写出函数的类型。不过,OCaml常常会输出它所认为的函数的类型,所以你需要知道它的语法。对于一个带有参数arg1arg2、……的函数,编译器会输出:

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

它是如何做到的呢?首先它看在哪里用到了ab,即在表达式(a +. b)中。现在,+.则是一个总是输入两个float参数,所以通过简单的探测,ab一定都是float类型的。

第二,/.函数返回一个float,同时它也是average的返回值,所以,average必然返回一个float。最后结果就是average会拥有以下类型签名:

average : float -> float -> float

类型推断明显对于这类短小的程序很方便,不过甚至对于大型程序,都很有效,同时它是个重要的省时特点,因为它可以避免其他语言中的一大类错误,如段错误(segfault)、的NullPointerExceptionClassCastException(或者是一些重要的但常常被忽略的运行时警告,如Perl)。

Posted in OCaml | 2 Comments »

JavaScript高级程序设计

Posted by ShiningRay on 8th 九月 2006

profession-javascript-cover.jpg

基本信息

  • 【英文名】Professional JavaScript for Web Developers
  • 【作者】Nicholas C. Zakas
  • 【译者】曹力 张欣
  • 【ISBN】7115152098
  • 【出版时间】2006-9-15
  • 【页码】670
  • 【原出版社】Wrox
  • 【简介】本书从最早期Netscape浏览器中的Javascript开始讲起,直到当前它对XML和Web服务的具体支持,展示了如何充分利用这种功能强大的语言开发自己的应用程序,以解决当今Web开发者面对的商业问题。
  • 活动主页

译者序

Read the rest of this entry »

Posted in JavaScript, 书籍 | 4 Comments »

Objective CAML系列教程

Posted by ShiningRay on 7th 九月 2006

内容翻译自Object CAML Tutorial,ShiningRay翻译

OCaml是一个高速、简练和强大的用于应用开发的语言——不过我假设你已经知道这点了,并且你已经设法安装好了它。否则的话,你可以在OCaml官方网站或在OCaml Alliance’s getting started area上找到更多信息。

教程