シラバスのここまでのところで、すでに多くの関数定義を見てきました。 たいていの場合、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)の引数はふつう2つの別々のものと考えますが、コンパイラーには1つのタプルとして見えています。
これは引数が1つの場合にも当てはまります(ただし、それをはっきりさせるにはカンマを付ける必要があります)。
julia> typeof( (2, 3) )
Tuple{Int64, Int64}
julia> typeof( (2,) )
Tuple{Int64}
関数をブラックボックスとして見ると、タプルが入力され、戻り値が出てくると言えます。 より正確には、タプルは戻り値に_マッピング_されます。
これはふつう「ポリモーフィズム」と呼ばれます。
これは何十年にもわたって計算機科学の大きなテーマであり、さまざまなアプローチが採られてきました。
Juliaのアプローチは独特ですが、唯一のものではありません。
+演算子は現在、161個のメソッドを持つ関数です)。...を使えば、これをある程度回避できます)。dispatchedされ、結果を返します。MethodErrorが発生します。Juliaは動的型付けなので、メソッドのディスパッチは_実行時_に行われることに注意してください。
前のセクションは言葉ばかりだったので、これらの考え方をコードに置き換えてみましょう。 簡単にするため、以下の関数は引数を1つだけにしていますが、何十もの引数を持つ関数(たとえばグラフ描画パッケージや機械学習のもの)も珍しくありません。
まず、図形を表すカスタムの型をいくつか定義しましょう。
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
次に、それぞれの図形の面積を計算するメソッドを定義します。 どのメソッドも引数を1つ取り、それぞれ異なる、ユーザー定義の具象型を受け取ります。
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に制約を付けることで、もっと具体的にできます。
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のプログラマーは型を狭く指定したメソッドを定義することを目指します。
2つの整数を足すことと2つの小数を足すことは、バイトコードのレベルではまったく異なります。さらに、整数と小数を足す場合は、型プロモーションという厄介さが加わります。
型が具体的であるほど、コンパイラーはより多くの最適化を行えます。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))."
まずは、猫と犬が出会ったときに何が起こるかをシミュレーションします。
まず、抽象型Petを追加します。
次に、PetのサブタイプとしてCatとDogという型を定義します。
どちらもnameという1つのフィールドを持ちます。
ペットの名前を返す関数name()を実装します。
julia> fido = Dog("Fido")
Dog("Fido")
julia> name(fido)
"Fido"
次の出会いに対するmeets()メソッドを実装します。
julia> ginger = Cat("Ginger")
Cat("Ginger")
julia> meets(ginger, fido)
"hisses"
2つの名前を持つ実体が出会ったときに何が起こるかを報告する、1つのメソッドとしてencounter()関数を実装します。
julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."
散歩の途中で、馬のような別のペットに出会ったらどうなるでしょうか?
julia> bella = Horse("Bella")
Horse("Bella")
julia> encounter(fido, bella)
"Fido meets Bella and is cautious."
ペット以外にも、ペットが出会うかもしれないものはたくさんあります。車、人間、植物、自然災害、小惑星…。そのときはどうなるのでしょうか?
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")
julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."
このシミュレーションでは、ほかにもいろいろな出会いが起こりえます。 車が犬に出会ったり、2つの電子が出会って相互作用したり…。
あらゆる出会いをあらかじめ網羅するのは不可能なので、汎用的なフォールバックを実装します。
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.