遭遇

遭遇

学習演習

はじめに

シラバスのここまでのところで、すでに多くの関数定義を見てきました。 たいていの場合、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個のメソッドを持つ関数です)。
  • 関数が呼び出されると、Juliaは呼び出しシグネチャ(引数の数と、それぞれの現在の型)を調べます。
  • 呼び出しシグネチャに最もよく一致するメソッドが選ばれます。
    • 一般に、引数の数は完全に一致している必要があります(ただし、splat/slurp演算子...を使えば、これをある程度回避できます)。
    • 呼び出しシグネチャと型が完全に一致するメソッドが優先されます。
    • 必要に応じて、Juliaは呼び出しシグネチャの各引数のスーパータイプを調べ、最もよく一致する、最も具体的なメソッドを探します。
  • 選ばれたメソッドは、与えられた引数で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))."

まずは、猫と犬が出会ったときに何が起こるかをシミュレーションします。

1. 独自の型を定義する

まず、抽象型Petを追加します。

次に、PetのサブタイプとしてCatとDogという型を定義します。 どちらもnameという1つのフィールドを持ちます。

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. 2つの実体のあいだの出会いを定義する

2つの名前を持つ実体が出会ったときに何が起こるかを報告する、1つのメソッドとして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. 汎用的なフォールバックを定義する

このシミュレーションでは、ほかにもいろいろな出会いが起こりえます。 車が犬に出会ったり、2つの電子が出会って相互作用したり…。

あらゆる出会いをあらかじめ網羅するのは不可能なので、汎用的なフォールバックを実装します。

  • 2つの未知のものや存在が出会うと、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を学んでマスターできます。すべて無料です。