12.7 函数的参数化

我们在定义一个类型的时候可以使其包含参数,并以此实现极大的灵活性和扩展能力。这样的定义不止可以表示单个的类型,还可以表示一个完整的类型族群。如此定义出来的类型也被称为参数化类型。在 Julia 语言中,到处都充斥着大量的参数化类型。不但如此,针对函数的参数化也是相当普遍的。

对于函数来说,参数化的意义主要在于,确定其结果与其参数之间在类型约束方面的对应关系。我们下面就以之前编写的一个sum3方法作为开始,讲解函数的参数化。我们先来回顾一下这个方法的定义。

  1. sum3(a, b) = a + b

我们可以看到,这个函数定义对于参数的类型没有任何的限制。同时,它也没有对结果进行声明。

尽管函数+的衍生方法众多,但终归还是有不适用的类型存在的,如Char类型和String类型等等。所以,我们还是应该对这个sum3方法有所约束,不能让它对什么类型的参数值都可以接受。下面是我对它的改写,以及新的 REPL 环境对它的解析:

  1. julia> sum3(a::T, b::T) where {T<:Number} = a + b
  2. sum3 (generic function with 1 method)
  3. julia>

在对函数进行参数化定义的时候,我们需要把where关键字以及相关的内容写在函数签名的右侧。如果函数的定义是用简洁形式编写的,那么它们还应该处于符号=的左边。

还记得吗?我们在讲参数化类型的时候介绍过针对这种类型的值化表示法。这种表示法使用where关键字来引领针对类型参数的范围约束,如Drawer{T} where T<:Jewelry。很显然,函数定义的参数化方式与之是类似的。

由于改造后的sum3方法的两个参数类型都被声明为了占位符T,因此它现在只能够接受类型相同的两个参数值。又由于存在T<:Number,所以我们传给它的两个参数值的类型还必须都是Number类型的某个子类型。否则,Julia 就会立即抛出一个MethodError类型的错误,并会告诉我们它没有找到与之相匹配的衍生方法。例如:

  1. julia> sum3(1, 2.3)
  2. ERROR: MethodError: no method matching sum3(::Int64, ::Float64)
  3. Closest candidates are:
  4. sum3(::T, ::T) where T<:Number at REPL[1]:1
  5. Stacktrace:
  6. [1] top-level scope at REPL[2]:1
  7. julia> sum3('1', '2')
  8. ERROR: MethodError: no method matching sum3(::Char, ::Char)
  9. Stacktrace:
  10. [1] top-level scope at REPL[3]:1
  11. julia>

同样的类型约束也可以被用在函数的结果声明上。不过,在这种情况下,我们就不能使用简洁形式去定义函数了。以下是我对前述sum3方法进行的第二次改造:

  1. julia> function sum3(a::T, b::T)::T where {T<:Number}
  2. a + b
  3. end
  4. sum3 (generic function with 1 method)
  5. julia>

这一版的定义可以明确地告诉使用者,它返回的结果值会与它接受的参数值拥有相同的类型。请注意,sum3函数至此仍然只有一个衍生方法。因为 Julia 的多重分派机制并不会依据函数定义中的结果声明去识别和分辨衍生方法。这使得这一版的sum3方法覆盖掉了上一版的sum3方法。

除此之外,参数化的函数参数类型不仅可以是T,还可以是Vector{T}Array{T,N}Dict{K, V}等等。当然了,如果这里存在多个类型参数占位符,那么我们还需要在函数签名的右侧追加多个where以及针对相应的类型参数的范围约束。例如:

  1. julia> op1 = Dict("a"=>1, "b"=>2, "c"=>3); op2 = 10;
  2. julia> function add_value!(d::Dict{K,V}, v::V)::Dict{K,V} where {K} where {V<:Number}
  3. for (key, num) in d
  4. d[key] = num + v
  5. end
  6. d
  7. end
  8. add_value! (generic function with 1 method)
  9. julia> add_value!(op1, op2)
  10. Dict{String,Int64} with 3 entries:
  11. "c" => 13
  12. "b" => 12
  13. "a" => 11
  14. julia>

关于函数定义的参数化,我已经把所有的基本方式都展示在这里了。怎么样?还是很简单的吧?至于更高级的玩法,你或许可以在“参数化类型”一章里的相关内容之中找到灵感。