
# ------------------------------------------------------------------------------------------
# # 多重派发
# 在这个 notebook 中我们将探索 Julia 的重要特性之一：**多重派发**。
#
# 多重派发让软件更**通用（generic）、更快**！
#
# ## 目录
#
# - [从熟悉的开始讲](#从熟悉的开始讲)
# - [指定传入变量的类型](#指定传入变量的类型)
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## 从熟悉的开始讲
#
# 从我们已经接触过的知识来理解多重派发。
#
# 我们可以声明一个 Julia 函数而不用给出传入变量的任何信息：
# ------------------------------------------------------------------------------------------

f(x) = x^2

# ------------------------------------------------------------------------------------------
# Julia 自会判断哪些传入变量是合理的，哪些又是不合理的：
# ------------------------------------------------------------------------------------------

f(10)

f([1, 2, 3])

# ------------------------------------------------------------------------------------------
# ## 指定传入变量的类型
#
# 但是，我们*也可以*明确指定传入变量的类型。
#
# 比如，我们写一个只接受字符串类型传入变量的函数 `foo`。
# ------------------------------------------------------------------------------------------

foo(x::String, y::String) = println("My inputs x and y are both strings!")

# ------------------------------------------------------------------------------------------
# 可以看到，要限制 `x` 和 `y` 的类型为 `String`，只要在形参名后面加上两个冒号和关键字 `String` 就行了。
#
# 现在 `foo` 只有在传入变量为 `String` 类型才正常工作。
# ------------------------------------------------------------------------------------------

foo("hello", "hi!")

foo(3, 4)

# ------------------------------------------------------------------------------------------
# 要让 `foo` 函数接受整型（`Int`）的传入变量，只要在声明 `foo` 函数时在形参后面加上 `::Int`。
# ------------------------------------------------------------------------------------------

foo(x::Int, y::Int) = println("My inputs x and y are both integers!")

foo(3, 4)

# ------------------------------------------------------------------------------------------
# 现在函数 `foo` 能接受整型变量了！但是注意，函数 `foo` 仍然能接受字符串变量！
# ------------------------------------------------------------------------------------------

foo("hello", "hi!")

# ------------------------------------------------------------------------------------------
# 这就涉及到多重派发的核心了。当我们声明
#
# ```julia
# foo(x::Int, y::Int) = println("My inputs x and y are both integers!")
# ```
# 我们并没有重写或覆盖
# ```julia
# foo(y::String, y::String)
# ```
# 而是给叫做 `foo` 的**通用函数（generic function）**添加了一个**新方法（method）**。
#
# 一个**通用函数** 是一个表示特定操作的抽象概念。
# 举个例子，通用函数 `+` 代表了加法这个概念
#
# 一个**方法** 是一个**通用函数** 接受*特定参数类型* 的一个具体实现。
# 举个例子，`+` 有不同的方法以接受浮点数、整型、矩阵等等。
#
# 我们可以通过函数 `methods` 来查看函数 `foo` 有多少种方法。
# ------------------------------------------------------------------------------------------

methods(foo)

# ------------------------------------------------------------------------------------------
# 猜猜看：加法有多少种方法？
# ------------------------------------------------------------------------------------------

methods(+)

# ------------------------------------------------------------------------------------------
# 所以，我们现在可以在调用 `foo` 的时候传入整型或字符串了。
# 当你调用 `foo` 时传入特定组合的参数时，Julia 将根据传入参数的类型派发对应的方法。*这* 就是多重派发。
#
# 多重派发让我们的代码更加通用更加快。
# 代码更加通用更灵活是因为我们是在写代码描述抽象操作例如加法和乘法，而不是描述具体的实现。
# 同时，因为 Julia 可以为相关类型调用高效的方法，代码可以运行得很快。
#
# 想查看我们调用一个通用函数时派发的是哪个方法，我们可以用宏 `@which`：
# ------------------------------------------------------------------------------------------

@which foo(3, 4)

# ------------------------------------------------------------------------------------------
# 让我们用 `@which` 来看看加号！
# ------------------------------------------------------------------------------------------

@which 3.0 + 3.0

# ------------------------------------------------------------------------------------------
# 可以继续给通用函数 `foo` 增加方法。试试接受**抽象类型** `Number` 作为参数，它包括子类型 `Int`、`Float64` 等数字类型：
# ------------------------------------------------------------------------------------------

foo(x::Number, y::Number) = println("My inputs x and y are both numbers!")

# ------------------------------------------------------------------------------------------
# `foo` 的这个方法可以接受如浮点数作为参数：
# ------------------------------------------------------------------------------------------

foo(3.0, 4.0)

# ------------------------------------------------------------------------------------------
# 我们还可以给 `foo` 加上一个保险的、鸭子类型的方法，让它可以接受任意类型输入：
# ------------------------------------------------------------------------------------------

foo(x, y) = println("I accept inputs of any type!")

# ------------------------------------------------------------------------------------------
# 到目前为止，这个方法会在我们给 `foo` 传入非数字参数时被调用：
# ------------------------------------------------------------------------------------------

v = rand(3)
foo(v, v)

# ------------------------------------------------------------------------------------------
# ## 练习
#
# ### 9.1
#
# 拓展通用函数 `foo`，给它加上一个只接受1个 `Bool` 类型参数的方法，使它打印"foo with one boolean!"
# ------------------------------------------------------------------------------------------

# 在此作答
function foo()

end

# ------------------------------------------------------------------------------------------
# 确保这个你写的方法在你执行 `foo(true)` 被派发。
# ------------------------------------------------------------------------------------------

@assert foo(true) == "foo with one boolean!"

# ------------------------------------------------------------------------------------------
# 请在完成练习运行 `@assert` 块以检测结果是否正确，若无报错即为正确。
# ------------------------------------------------------------------------------------------
