Polymorphism
您肯定注意到了我们创造的有些类型是高度相似的。比如说,我们创建如下类型的列表:
type list_int =
| EndListInt
| Int of int * list_int
type list_string =
| EndListString
| String of string * list_string
其中唯一的区别就是序列中元素的类型不同(一个为int
,另一个为string
)。在这两种类型之间,有很多函数是可以共用的,比如用于计算列表大小的函数,因为该函数的使用与列表中元素类型无关。
在这种情况下,为了避免重复,OCaml提供了一个解决方法:polymorphism。
Type generic (泛型类型)
为了创建一个泛型类型,我们需要一个或数个类型的参数,就像函数由值参数化一样。当我们定义泛型类型时,我们不会去写‘真正的’类型,而是用一个以撇号开头的类型名称代替(按照惯例,我们会使用字母命名:'a
,'b
等)。
type 'a list =
| End
| Element of 'a * list
在这个例子中,我们在type
和list
中间加入了'a
,并且我们也用'a
取代了int
或string
。
接下来,我们将使用'a
来取代任意‘真实的’类型。比如说,我们需要一个int
的列表,我们就会使用类型int list
。如果是string
的列表,那么就会变成string list
,等等。
(* 计算列表中元素的和 *)
let rec sum (l : int list) : int =
match l with
| End -> 0
| Element(x, rest) -> x + (sum rest)
(* 连接列表中所有的字符 *)
let rec concat (l : string list) : string =
match l with
| End -> ""
| Element(x, rest) -> x ^ (concat rest)
多态函数
函数同样可以直接操作泛型类型。如果我们不需要一个特定类型的话,我们同样可以写泛型函数。再回到泛型列表的例子,计算列表大小与其元素类型无关。所以我们可以直接写一个参数为'a list
的函数:
let rec size (l : list) : int =
| End -> 0
| Element(_, rest) -> 1 + (size rest)
Bonus: 许多类型的参数
我们还可以使用括号和逗号来在参数中指定多种类型。举个例子,我们可以创建一个泛型类型来指出一个操作是否成功:
type ('a, 'b) result =
| Success of 'a
| Failure of 'b
(* 用法举例:从讨论组中截取一条信息,
* 可能会有很多种失败的原因 *)
type error =
| NoConnection (* 当我们忘记开4G的时候 *)
| NotAllowed (* 比如说讨论组是私人的*)
type message = string
(* 这个函数要么为Success并返回一条信息,
* 要么为Failure并返回error* )
let get_message : (message, error) result =
let show_message =
match get_message with
| Success(msg) -> "New message : " ^ msg
| Failure(err) -> match err with
| NOConnection -> "Error : no connection"
| NotAllowed -> "Error : 禁止访问该讨论组"
课后练习
以下是一些有关polymorphism的练习。这些练习会比之前的抽象得多,但我们别无选择:
当开始处理泛型类型时,我们必须要思考!
练习1
我们要定义一个optional
类型,用来指出一个数值是否有出现的可能性。
type 'a optional =
| None (* 什么也没有 *)
| Some of 'a (* 有一个值 *)
这些函数的类型为?
let sth_or_then opt by_default =
match opt with
| Some(x) -> x
| None -> by_default
<答案点我>
'a optional -> 'a -> 'a
let contains opt x =
match opt with
| None -> false
| Some(y) -> = y
<答案点我>
'a optional -> 'a -> bool
let multiply opt x =
match opt with
| None -> None
| Some(a) -> Some(a *. x)
<答案点我>
float optional -> float -> float optional
练习2
现在我们要创建一个dico
类型,该类型带有两个不同类型参数:一个为键,另一个为值。该类型中,一个值对应一个键,所以我们可以根据其键来找到值。该类型有点像Python中的字典,唯一的区别在于,这里所有的键为同一类型,所有的值为同一类型,两者不可混合。
因此,我们要:
-
定义`dico`类型(注意:该类型为多态且递归);
-
写一个名为`create`的函数,用于创建一个空的`dico`;
-
写一个名为`define`的函数,用于将一个键和一个值加入到`dico`的末尾;
-
写一个名为`obtain`的函数,该函数可以通过键来找到`dico`中与之关联的值;
-
bonus:写一个名为`delete`的函数,该函数可以将某个指定键从字典中删除。
<答案点我>
type ('k, 'v) dico =
| Empty
| Element of 'k * 'v * ('k, 'v) dico
let create = Empty
let rec define (key : 'k) (value : 'v) (dic : ('k, 'v) dico) : ('k, 'v) dico =
match dic with
| Element(_, _, rest) -> define key value rest
| Empty -> Element(key, value, Empty)
let rec obtain (key : 'k) (dic : ('k, 'v) dico) : 'v =
match dic with
| Empty -> failwith "Key not found"
| Element(k, v, rest) ->
if key = k then
v
else
obtain key rest
let rec delete (key : 'k) (dic : ('k, 'v) dico) : ('k, 'v) dico =
match dic with
| Empty -> Empty
| Element(k, v, rest) ->
if k = key then
rest
else
Element(k, v, delete key rest)