
# ------------------------------------------------------------------------------------------
# # 多重派发
# 在这个 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 3.0 + 3.0

# ------------------------------------------------------------------------------------------
# 考虑到一个特别写给浮点数的方法派发给了 `3.0 + 3.0`，生成的 LLVM 代码极其简练：
# ------------------------------------------------------------------------------------------

@code_llvm 3.0 + 3.0

# ------------------------------------------------------------------------------------------
# 要注意的是 Julia 在我们写一般的函数定义时也很快，因为具体的、定制的方法最终是在后台调用的。
#
# 举个例子，我们声明加法函数 `myadd` 时没有类型注释——
# ------------------------------------------------------------------------------------------

myadd(x, y) = x + y

# ------------------------------------------------------------------------------------------
# 虽然我们没有限制 `x` 和 `y` 的类型，但是 `myadd(3.0, 3.0)` 生成的 LLVM 代码和 `3.0 + 3.0` 差不多
# ------------------------------------------------------------------------------------------

@code_llvm myadd(3.0, 3.0)
