遭遇

遭遇

学习练习

简介

学到教学大纲这一步,你已经见过很多函数定义。通常,这些定义在 REPL 中展示时,并没有对输出做太多说明:

julia> f(x) = x^2
f (generic function with 1 method)

为什么会显示 "generic function with 1 method"?

假设我们想更具体地指定实参类型。

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

或者,我们也可以改变实参的个数:

julia> f(x, y) = x * y
f (generic function with 4 methods)

看来,同一个函数的这些新定义被称为“方法”。方法会被加到一个列表里,而不是覆盖旧的定义。

我们可以很方便地列出这些方法:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

当我们调用函数f时,Julia 能以某种方式知道该用哪个方法:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

什么是函数?

根据手册,函数是一种对象,它把实参组成的元组映射为一个返回值。

我们通常会把f(2, 3)中的函数实参看作两个独立的东西,但编译器把它们看作一个元组。即使只有一个实参也是如此(不过需要加一个逗号才能看得更清楚):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

把函数看作一个黑箱,我们可以说进去的是一个元组,出来的是一个返回值。更正式地说,元组被_映射_为返回值。

函数如何处理不同数量和类型的实参?

这通常被称为“多态”。

几十年来,这一直是计算机科学中的一个重要课题,也出现了各种不同的处理方式。

Julia 的做法很不寻常,但并非独此一家。

  • 函数是“泛型”的,也就是说一个函数可以挂载多个方法。
  • 在挂上方法之前,函数只是一个名字。
  • 方法可以按需添加,数量可能很大(目前+运算符就是一个拥有 161 个方法的函数)。
  • 调用函数时,Julia 会检查调用签名:实参的个数以及每个实参当前的类型。
  • 类型与调用签名最匹配的方法会被选中。
    • 一般来说,实参个数必须完全一致(不过 splat/slurp 运算符...可以部分绕过这一限制)。
    • 类型与调用签名完全匹配的方法优先。
    • 必要时,Julia 会查看调用签名中每个实参的父类型,以找到匹配最接近、最具体的方法。
  • 被选中的方法会带着给定的实参被dispatched,并返回结果。
  • 如果没有找到合适的方法,就会抛出一个MethodError。

注意,Julia 是动态类型的,因此方法分派发生在_运行期_。

多重分派:一个例子

上一节文字很多,下面我们把这些想法变成代码。为简单起见,下面的函数都只有一个实参,但拥有几十个实参的函数并不少见(比如绘图包或机器学习中的函数)。

先定义几个自定义类型,用来表示几何图形。

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

接下来,我们可以定义计算各种图形面积的方法。每个方法都接收一个实参,而且各自接收的都是不同的具体用户自定义类型。

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

现在我们可以创建一些图形并计算面积,依靠 Julia 的分派机制为每个图形选用正确的公式。

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

参数化方法

方法签名可以接受占位的类型值,方式和参数化类型大致相同。

这种语法很有特点,需要用到where子句:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

上面的例子要求两个实参使用同一个类型T,无论T具体是什么。

我们可以给T加上约束,让它更具体:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

有了这个方法,只有完全相同的数值类型才会返回true。

也可以有多个占位变量,在where子句中用逗号分隔。

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

总结

多重分派在 Julia 中非常灵活、性能也很高,因此被广泛使用。

正如+拥有 161 个方法这个例子所示,Julia 程序员会力求让方法所指定的类型尽量窄。在字节码层面,两个整数相加与两个浮点数相加差别很大,而整数与浮点数相加还会多出类型提升这一层复杂性。

类型越具体,编译器能做的优化就越多:在 Exercism 的小练习里这无关紧要,但在 Julia 所擅长的大型数值模拟中,这对性能至关重要。

如果你知道一个方法会接收到什么输入,请告诉编译器!

说明

在本练习中,你将实现一个相遇的模拟。这会让你熟悉多重分派的基础知识,这是 Julia 的主要范式。

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

一般来说,相遇指的是一个实体a遇到另一个实体b,并对其做出反应。

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

首先,我们会模拟猫和狗相遇时会发生什么。

1. 定义自定义类型

首先添加一个抽象类型Pet。

然后定义类型Cat和Dog,作为Pet的子类型。 每个类型都有一个字段name。

2. 获取宠物的名字

实现一个函数name(),返回宠物的名字。

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. 定义猫和狗相遇时会发生什么

为以下几种相遇实现meets()方法:

  • 两只狗相遇时,它们会互相sniff。
  • 猫遇到狗时,会对狗hisses。
  • 狗遇到猫时,会chases猫。
  • 两只猫相遇时,它们会slink。
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. 定义两个实体之间的相遇

用一个方法实现encounter()函数,用来报告两个有名字的实体相遇时会发生什么。

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. 为宠物之间的相遇定义兜底反应

如果它们在散步时遇到别的宠物,比如一匹马,会发生什么?

  • 如果一只宠物遇到另一只它不认识的宠物,它会is cautious。
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. 为宠物遇到不认识的东西时定义兜底

宠物还可能遇到很多并非宠物的其他东西:汽车、人类、植物、自然灾害、小行星……那会发生什么?

  • 如果一只宠物遇到它从未见过的东西,它会runs away。
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. 定义通用的兜底

我们的模拟中还可能出现很多其他相遇。 汽车遇到狗,两个电子相遇并相互作用……

不可能提前覆盖所有相遇,所以我们要实现一个通用的兜底。

  • 如果两个未知的事物或存在相遇,nothing happens。
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 遭遇 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。