【译】【OCaml函数式编程教程】第六章:Polymorphism(多态性)

318 阅读4分钟

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

在这个例子中,我们在typelist中间加入了'a,并且我们也用'a取代了intstring

接下来,我们将使用'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)