関数

Julia において、関数 (function) とは引数の値からなるタプルを返り値に対応させるオブジェクトです。Julia の関数はプログラムのグローバルな状態を変更したり、グローバルな状態に影響を受けることがあるので、純粋ではありません。Julia における関数定義の基本的な構文は次の通りです:

julia> function f(x,y)
           x + y
       end
f (generic function with 1 method)

この関数は二つの引数 xy を受け取り、最後に評価された式の値、つまり x + y を返します。

Julia にはもう一つ、関数を定義するためのより簡潔な構文があります。上に示した伝統的な関数宣言は、次のコンパクトな "代入形式" (assignment form) の宣言と等価です:

julia> f(x,y) = x + y
f (generic function with 1 method)

この代入形式の宣言では関数の本体が一つの式である必要があります (ただし複合式でも構いません)。Julia では短い簡単な関数を定義することが多いので、タイプ量と視覚的なノイズを大きく減らせるこの短い関数定義の構文は非常によく使われます。

関数は伝統的な括弧を使った構文で呼び出します:

julia> f(2,3)
5

括弧のない f は関数オブジェクトを表します。関数オブジェクトは他の値と同じように他の変数への代入が可能です:

julia> g = f;

julia> g(2,3)
5

変数と同様、関数の名前には Unicode を利用できます:

julia> (x,y) = x + y
 (generic function with 1 method)

julia> (2, 3)
5

引数の受け渡し

Julia において、関数の引数は "共有による受け渡し" (pass-by-sharing) などと呼ばれる慣習に従います。つまり、関数に値が渡されるときコピーは起こりません。関数の引数自体は新しい変数束縛 (値に対する新しい呼び名) として振る舞いますが、この新しい変数が指す値は引数に渡された変数が指す値と同じです。関数内で変更可能な値 (例えば Array) を変更すれば、その変更は呼び出し側に伝わります。これは Scheme, 大部分の Lisp, Python, Ruby, Perl といった動的言語と同様の振る舞いです。

return キーワード

関数の返り値は最後に評価された式の値であり、デフォルトでは関数定義の本体に含まれる最後の式です。上で定義した関数 f では式 x + y の値が関数の返り値となります。これとは異なる方法として、Julia には関数をその場で返すためのキーワード return が用意されています。このキーワードの意味は他の言語と同様で、return に渡された式の値が返り値となってその時点で関数が返ります:

function g(x,y)
    return x * y
    x + y
end

対話セッションで関数を定義すれば、二つの方法を分かりやすく比較できます:

julia> f(x,y) = x + y
f (generic function with 1 method)

julia> function g(x,y)
           return x * y
           x + y
       end
g (generic function with 1 method)

julia> f(2,3)
5

julia> g(2,3)
6

もちろん、g のような完全に線形な関数では return は不必要です: return の後の式 x + y は決して実行されないので、x * y とだけ書けば同じ関数が得られます。しかし他の制御構造構文が使われると、return が不可欠になります。例えば次に示すのは、直角をなすニ辺の長さが xy である直角三角形の斜辺の長さをオーバーフローを起こさずに計算する関数です:

julia> function hypot(x,y)
           x = abs(x)
           y = abs(y)
           if x > y
               r = y/x
               return x*sqrt(1+r*r)
           end
           if y == 0
               return zero(x)
           end
           r = x/y
           return y*sqrt(1+r*r)
       end
hypot (generic function with 1 method)

julia> hypot(3, 4)
5.0

この関数は三つの return を持ち、xy の値に応じて三つの異なる式の値を返します。なお最後の行の return は最後の式なので、省略可能です。

返り値の型

返り値の型は関数宣言に :: 演算子を付けて指定できます。この演算子を付けると、返り値が指定した方に変換されます:

julia> function g(x, y)::Int8
           return x * y
       end;

julia> typeof(g(1, 2))
Int8

この関数は xy の型に関わらず必ず Int8 を返します。返り値の型について詳しくは型宣言の節を参照してください。

返り値 nothing

値を返さない関数 (副作用のためだけに使われる関数) では返り値を nothing とするのが Julia の慣習です:

function printx(x)
    println("x = $x")
    return nothing
end

これが慣習であるとは、nothing が Julia のキーワードではなく、Nothing 型のシングルトンオブジェクトであるという意味です。なお上記の printx 関数が不自然であることに気が付いたかもしれません: printlnnothing を返すので、return の行は冗長です。

return nothing を短縮して書く方法は二つあります。一つは return とだけ書く方法で、こうすると nothing が返るものとして扱われます。もう一つは nothing とだけ書く方法で、こうすると関数の最後の式を返り値とする規則により nothing が返ります。return nothing, return, nothing のどれを使うかはコーディングスタイルの問題です。

演算子は関数である

Julia の大部分の演算子は特殊な記法がサポートされた関数に過ぎません (例外は特別な評価意味論を持った &&|| です。短絡評価の要請により、こういった演算子ではオペランドの評価と演算子の評価を並行して行う必要があるので、関数として扱うことができません)。関数である演算子は括弧を使った引数リスト渡すことでも呼び出せます。こうして呼び出すと見た目は通常の関数と変わりません:

julia> 1 + 2 + 3
6

julia> +(1,2,3)
6

中置記法は括弧を使った関数適用と完全に等価です ──実際、中置記法は内部で関数の呼び出しとしてパースされます。これは +* といった演算子を他の関数と同様に代入したり引数に渡したりできることも意味します:

julia> f = +;

julia> f(1,2,3)
6

ただし名前を f とした場合には、中置記法はサポートされません。

特殊な名前を持った演算子

一部の特殊な演算子および式では、見た目からは想像が付かない名前の関数が呼び出されます。式と呼び出される関数の対応を示します:

呼び出される関数
[A B C ...] hcat
[A; B; C; ...] vcat
[A B; C D; ...] hvcat
A' adjoint
A[i] getindex
A[i] = x setindex!
A.n getproperty
A.n = x setproperty!

無名関数

Julia の関数はファーストクラスのオブジェクトです: 関数は変数へ代入でき、関数が代入された変数に対して通常の関数呼び出し構文を適用すれば関数が呼び出され、関数の引数や返り値を関数にできます。さらに名前の付いていない無名関数 (anonymous functions) を作ることもできます。無名関数の構文は次の通りです:

julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)

julia> function (x)
           x^2 + 2x - 1
       end
#3 (generic function with 1 method)

この例では一つの引数 x を受け取ってその値における多項式 x^2 + 2x - 1 の値を返す関数を作成しています。作成されるのは総称関数 (generic function) ですが、コンパイラによって生成される連番の名前が付いている点に注意してください。

無名関数は関数を引数に取る関数で主に利用されます。こういった関数の古典的な例は、配列の各要素に対して関数を適用する map 関数です。map は結果の適用結果からなる新しい配列を返します:

julia> map(round, [1.2, 3.5, 1.7])
3-element Array{Float64,1}:
 1.0
 4.0
 2.0

変形を行う関数に名前が付いているなら、それを map の第一引数に渡せば問題ありません。しかし、行いたい処理に名前が付いていない場合もよくあります。このような場合に無名関数を使えば、名前を付けることなく使い捨ての関数オブジェクトを簡単に作成できます:

julia> map(x -> x^2 + 2x - 1, [1, 3, -1])
3-element Array{Int64,1}:
  2
 14
 -2

複数の引数を取る無名関数は (x,y,z)->2x+y-z のように書き、引数を取らない無名関数は ()->3 のように書きます。引数の無い関数は変に思えるかもしれませんが、計算を "遅延" させるときに有用です。つまり何らかのコードブロックをゼロ引数の関数で包み、後でそれを呼び出して処理を実行するということです。

ゼロ引数の無名関数の使用例として、get 関数への次の呼び出しを考えます:

get(dict, key) do
    # key が見つからなかった場合のデフォルト値を計算する処理
    time()
end

後で詳しく説明しますが、上記のコードは doend の間にある処理を行う無名関数を第一引数として get を呼び出すのと等価です:

get(()->time(), dict, key)

こうすると time の呼び出しがゼロ引数の無名関数で包まれることで遅延され、keydict に存在しないときにだけ time が呼ばれるようになります。

タプル

Julia には関数の引数・返り値と密接な関係を持つタプル (tuple) と呼ばれる組み込みのデータ構造があります。タプルは任意の値を保持できる固定長コンテナですが、不変 (immutable) であり、改変できません。タプルは括弧とコンマを使って作成し、添え字を使ってアクセスします:

julia> (1, 1+1)
(1, 2)

julia> (1,)
(1,)

julia> x = (0.0, "hello", 6*7)
(0.0, "hello", 42)

julia> x[2]
"hello"

長さ 1 のタプルにはコンマが必要なことに注意してください。(1) は括弧に囲まれた式でしかないので、(1,) と書かなければなりません。また () は空の (長さ 0 の) タプルを表します。

名前付きタプル

タプルの要素には名前を付けることもできます。タプルに名前を付けると名前付きタプル (named tuple) が作成されます:

julia> x = (a=2, b=1+2)
(a = 2, b = 3)

julia> x[1]
2

julia> x.a
2

名前付きタプルはタプルと非常によく似ていますが、唯一名前を使ったドット構文 (x.a) でフィールドにアクセスできる点が異なります。名前付きタプルには通常の数字の添え字を使ったアクセス (x[1]) も可能です。

複数の返り値

Julia の関数から複数の値を返す方法はありませんが、タプルを返すことでこの挙動をシミュレートできます。さらにタプルは括弧を使わずに作成・分割できるので、まるで複数の値を関数とやり取りしているかのようにコードを書くことができます。例えば次の関数は値の組を返します:

julia> function foo(a,b)
           a+b, a*b
       end
foo (generic function with 1 method)

返り値を変数に代入することなく対話セッションでこの関数を呼び出せば、返り値がタプルであることが分かります:

julia> foo(2,3)
(5, 6)

しかしこういった関数は通常、それぞれの返り値を変数に代入するために使います。Julia にはタプルを "分割" するための機能があります:

julia> x, y = foo(2,3)
(5, 6)

julia> x
5

julia> y
6

return キーワードを使っても複数の値を返せます:

function foo(a,b)
    return a+b, a*b
end

これは一つ前の foo の定義と全く同じです。

引数の分割

分割の機能は関数の引数でも使うことができます。関数の引数の名前がシンボルではなく (x, y) という形をしていれば、(x, y) = 引数 という代入が自動的に挿入されます:

julia> minmax(x, y) = (y < x) ? (y, x) : (x, y)

julia> gap((min, max)) = max - min

julia> gap(minmax(10, 2))
8

gap の定義にある二重の括弧に注目してください。二重の括弧が無ければ gap は二引数の関数であり、この例のように呼び出すことはできません。

可変長引数関数

可変個の引数を受け取る関数が書けると便利な場合がよくあります。そういった関数は伝統的に「可変長引数 (varargs) 関数」と呼ばれます (varargs は "variable number of arguments" (可変個の引数) の略です)。最後の位置引数の後にドットを三つ書くと可変長引数関数を定義できます:

julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)

bar を呼び出すと、変数 a, b は通常通り最初の二つの引数に束縛され、変数 x は二つ目以降の (ゼロ個以上の) 引数を持つ反復可能コレクションに束縛されます:

julia> bar(1,2)
(1, 2, ())

julia> bar(1,2,3)
(1, 2, (3,))

julia> bar(1, 2, 3, 4)
(1, 2, (3, 4))

julia> bar(1,2,3,4,5,6)
(1, 2, (3, 4, 5, 6))

この実行例で xbar に渡された二つ目以降の値からなるタプルに束縛されています。

可変長引数として渡される値の個数は制限できます。プログラムによる可変長引数メソッドの制限の節で詳しく説明されます。

反対に、反復可能コレクションに含まれる値を崩して個別の引数として関数呼び出しに渡せると便利な場合がよくあります。これは関数呼び出しの側に ... を付ければ行えます:

julia> x = (3, 4)
(3, 4)

julia> bar(1,2,x...)
(1, 2, (3, 4))

この実行例では値が崩されたタプルがちょうど可変長引数となっています。しかし、崩された先が可変長引数でなくても構いません:

julia> x = (2, 3, 4)
(2, 3, 4)

julia> bar(1,x...)
(1, 2, (3, 4))

julia> x = (1, 2, 3, 4)
(1, 2, 3, 4)

julia> bar(x...)
(1, 2, (3, 4))

さらに、タプルでない反復可能オブジェクトも崩して関数呼び出しに渡すことができます:

julia> x = [3,4]
2-element Array{Int64,1}:
 3
 4

julia> bar(1,2,x...)
(1, 2, (3, 4))

julia> x = [1,2,3,4]
4-element Array{Int64,1}:
 1
 2
 3
 4

julia> bar(x...)
(1, 2, (3, 4))

また、崩した値を受け取る関数は可変長引数関数でなくても構いません (ただし実際にはたいてい可変長引数関数です):

julia> baz(a,b) = a + b;

julia> args = [1,2]
2-element Array{Int64,1}:
 1
 2

julia> baz(args...)
3

julia> args = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> baz(args...)
ERROR: MethodError: no method matching baz(::Int64, ::Int64, ::Int64)
Closest candidates are:
  baz(::Any, ::Any) at none:1

実行例から分かるように、崩されるコンテナに含まれる要素の個数が関数の引数の個数と合わないと関数呼び出しは失敗します。これは明示的に渡した引数が多すぎるときに呼び出しが失敗するのと同様です。

省略可能引数

関数の引数に適切なデフォルト値を提供できる場合も多くあります。デフォルト値を設定しておけば、ユーザーが関数を呼び出すたびに全ての引数を渡す手間を省けます。例えば Dates モジュールの Date(y, [m, d]) 関数は ymd 日を表す Date 型の値を作成しますが、md はデフォルト値が 1 の省略可能引数 (optional argument) となっています。この動作は次のように簡潔に表現できます:

function Date(y::Int64, m::Int64=1, d::Int64=1)
    err = validargs(Date, y, m, d)
    err === nothing || throw(err)
    return Date(UTD(totaldays(y, m, d)))
end

この Date 関数は定義の中でもう一度 Date を呼び出していますが、ここで呼び出されるのは引数が UTInstant{Day} 型の Date です。

このように定義された関数は引数の数を一個・二個・三個のいずれとしても呼び出すことができ、指定されない引数には自動的にデフォルトの値が渡されます:

julia> using Dates

julia> Date(2000, 12, 12)
2000-12-12

julia> Date(2000, 12)
2000-12-01

julia> Date(2000)
2000-01-01

実は省略可能引数は引数の数が異なる複数のメソッドをまとめて作成するための省略記法に過ぎません (参照: 省略可能引数とキーワード引数に関する注意点)。このことは上述の Date 関数に対して methods 関数を呼ぶと確認できます。

キーワード引数

関数の中には引数の数や可能な動作が多すぎて呼び出し方を覚えるのが難しいものがあります。キーワード引数 (keyword argument) を使って引数を位置ではなく名前で指定できるようにすれば、そういった複雑なインターフェースを使いやすくできます。

例として、直線をプロットする plot という関数を考えます。この関数は直線のスタイル・幅・色といった様々なオプションを持つかもしれません。そういった場合にキーワード引数を使って関数を定義すれば、幅だけを指定して plot(x, y, width=2) のように呼び出すことができます。意味のある名前を使って引数を指定できるので、関数の呼び出しが読みやすくなりました。さらに大量の引数の中で使うものだけを好きな順番で渡せるようにもなっています。

キーワード引数を持つ関数はシグネチャにセミコロンを入れて定義します:

function plot(x, y; style="solid", width=1, color="black")
    ###
end

関数を呼ぶときはセミコロンは省略できます: plot(x, y, width=2) でも plot(x, y; width=2) でも構いません。ただし前者のスタイルの方がよく使われます。セミコロンが必要になるのは可変長引数または別の場所で計算されたキーワード引数 (後述) が絡むときだけです。

キーワード引数のデフォルト値は必要なとき (キーワード引数が渡されないとき) にだけ、左から右の順番で評価されます。そのためデフォルト値の式はそれより左にあるキーワードと引数を参照できます。

キーワード引数の型は次のように指定します:

function f(;x::Int=1)
    ###
end

キーワード引数は可変長引数関数でも利用できます:

function plot(x...; style="solid")
    ###
end

余分なキーワード引数は可変長引数関数と同様に ... で収集できます:

function f(x; y=0, kwargs...)
    ###
end

f の中で kwargs は名前付きタプルに対するキーバリュー反復子となります。名前付きタプル (または Symbol 型のキーを持つ辞書) kwargs の前にセミコロンを付けて f(x, z=1; kwargs...) のようにすると、その名前付きタプルまたは辞書の要素を f のキーワード引数として渡すことができます。

キーワード引数にデフォルト値を指定しないと、その値は必須になります。呼び出し側が必須のキーワード引数に値を設定するのを忘れると UndefKeywordError 例外が発生します:

function f(x; y)
    ###
end
f(3, y=5) # y に値が渡されているので OK
f(3)      # -> UndefKeywordError(:y)

セミコロンの後に key => value という式を渡すこともでき、例えば plot(x, y; :width => 2)plot(x, y, width=2) と等価です。これはキーワードの名前が実行時に計算される場合に便利です。

セミコロンの後に識別子またはドット式が単体で続くと、その識別子またはフィールドの名前が暗黙にキーワード引数の名前として使われます。例えば plot(x, y; width)plot(x, y; width=width) と等価であり、plot(x, y; options.width)plot(x, y; width=options.width) と等価です。

キーワード引数の規則は同じ引数を複数回指定することを許しています。例えば plot(x, y; options..., width=2) としたとき option にも width に対する値が定義されていても構いません。このような場合には最も右にある値が優先されます。つまり今の例では width は必ず 2 です。ただしキーワード引数を plot(x, y, width=2, width=3) のように明示的に複数回指定すると構文エラーが発生します。

デフォルト値の評価スコープ

省略可能引数とキーワード引数に対するデフォルトの式を評価するときは、それより前にある引数だけがスコープに入ります。例えば

function f(x, a=b, b=1)
    ###
end

としたとき、a=b における b が指すのは外のスコープの b であり、この関数の引数 b ではありません。

関数の引数に対する do ブロック構文

関数を他の関数の引数として渡すのは強力なテクニックですが、そのときの構文はいつも便利とは限りません。特に引数の関数が複数行に渡ると、見た目が不格好になります。例えば場合分けのある関数を使って map を呼び出す場合がそうです:

map(x->begin
           if x < 0 && iseven(x)
               return 0
           elseif x == 0
               return 1
           else
               return x
           end
       end,
    [A, B, C])

Julia の予約語 do を使うと、このコードを分かりやすく書き換えることができます:

map([A, B, C]) do x
    if x < 0 && iseven(x)
        return 0
    elseif x == 0
        return 1
    else
        return x
    end
end

do x ... end の部分が x を引数とする無名関数となり、それが map の第一引数として渡されます。同様に do a, b ... end とすれば二引数の無名関数が作成され、do ... end とすれば () -> ... と同様の引数を取らない無名関数が作成されます。

無名関数の引数がどう初期化されるかは do ブロックの前にある "外側の" 関数によって決まります。今考えている map では xA, B, C が順番に設定されながら無名関数が呼ばれます。これは通常の map(func, [A, B, C]) という構文を使った場合と同様です。

do ブロック構文を使うと関数呼び出しが普通のコードブロックと変わらなくなるので、関数を使って言語を簡単に拡張できるようになります。do ブロック構文には map とは大きく異なる使い方も多くあり、システムの状態管理などのために使うことができます。例えば open 関数には、開いたファイルが最後に閉じられることを保証しながらコードを実行するバージョンがあります:

open("outfile", "w") do io
    write(io, data)
end

これは次の定義によって実現されます:

function open(f::Function, args...)
    io = open(args...)
    try
        f(io)
    finally
        close(io)
    end
end

ここで open は最初ファイルを書き込み用に開き、そうして得られる出力ストリームを do .. end で定義される無名関数に渡します。その関数が終了すると、open は最後にしっかりとストリームを閉じてから返ります。このとき無名関数が通常終了でも例外を発生させてもストリームは閉じられます (try/finally 構文は制御構造の章で説明されます)。

do ブロック構文を使うときは、ユーザーが定義する関数の引数がどのように初期化されるかをドキュメントまたは実装で確認するとよいでしょう。

ブロック内部で作られる通常の関数と同じように、do ブロックは外側のスコープの変数を「キャプチャ」できます。例えば上の open ... do の例では data という変数が外側のスコープからキャプチャされています。詳しくはパフォーマンス Tips で説明しますが、変数をキャプチャすると性能が落ちる可能性があります。

関数の合成とパイプ

Julia の関数は関数合成やパイプ (チェーン) で組み合わせることができます:

関数の合成 (composition) には合成演算子 を使います。(f ∘ g)(args...)f(g(args...)) と等価です。

合成演算子を REPL や適切に設定されたエディタで入力するには \circ-TAB とタイプしてください。

例えば、sqrt 関数と + 関数を合成するには次のようにします:

julia> (sqrt  +)(3, 6)
3.0

このコードはまず引数の和を求め、次にその和の平方根を計算します。

次の例では三つの関数を合成し、それを文字列の配列に対して map しています:

julia> map(first  reverse  uppercase, split("you can compose functions like this"))
6-element Array{Char,1}:
 'U': ASCII/Unicode U+0055 (category Lu: Letter, uppercase)
 'N': ASCII/Unicode U+004E (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
 'E': ASCII/Unicode U+0045 (category Lu: Letter, uppercase)
 'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)

関数をパイプ (チェーン) でつなぐと、一つ前の関数からの入力が次の関数の出力となります:

julia> 1:10 |> sum |> sqrt
7.416198487095663

この例では最初に sum 関数によって和が計算され、それが sqrt 関数に渡されます。同じ処理を合成を使って書けば次のようになります:

julia> (sqrt  sum)(1:10)
7.416198487095663

パイプ演算子は .|> でブロードキャストできます。これによりパイプ/チェーンとベクトル化を両方利用できます:

julia> ["a", "list", "of", "strings"] .|> [uppercase, reverse, titlecase, length]
4-element Array{Any,1}:
  "A"
  "tsil"
  "Of"
 7

関数をベクトル化するドット構文

技術計算言語では通常、関数の "ベクトル化" バージョンが用意されます。関数 f(x) のベクトル化バージョンを使うと、f を配列 A の各要素に適用してできる新しい配列を計算できます。ベクトル化の構文があるとデータ処理が楽になるだけではなく、言語によっては性能が向上します: ベクトル化された関数からは低水準言語で書かれた高速なライブラリコードを呼び出せるためです。しかし Julia では、ベクトル化された関数は性能のために必要ではありません。それどころか、普通にループを書いた方が性能が向上する場合もよくあります (参照: パフォーマンス Tips)。それでも関数のベクトル化が便利になる状況は存在するので、全ての Julia の関数 ff.(A) という構文で配列 (およびその他のコレクション) Aの各要素に対して適用できるようになっています。例えば sin をベクトル A の各要素に適用するには次のようにします:

julia> A = [1.0, 2.0, 3.0]
3-element Array{Float64,1}:
 1.0
 2.0
 3.0

julia> sin.(A)
3-element Array{Float64,1}:
 0.8414709848078965
 0.9092974268256817
 0.1411200080598672

もちろん f(A::AbstractArray) = map(f, A) として特殊化された「ベクトル化」メソッドを別に書けばドットを省略でき、こうしても f.(A) と同じ効率となります。f.(A) という構文を用意する利点は、関数をベクトル化すべきかどうかをライブラリの製作者が事前に考えないで済むところにあります。

より一般的に言うと f.(args...)broadcast(f, args...) と等価であり、複数の (一般に形が同じでない) 配列もしくは配列とスカラーの組み合わせに対する操作が可能です (参照: ブロードキャスト)。例えば f(x,y) = 3x + 4y のとき f.(pi,A) とすれば A の各要素 a に対する f(pi,a) を要素に持った新しい配列が返ります。あるいは f.(vector1,vector2) とすれば、全ての添え字 i に対する f(vector1[i],vector2[i]) を要素とする新しいベクトルが返ります (二つベクトルが異なる長さを持てば例外が発生します):

julia> f(x,y) = 3x + 4y;

julia> A = [1.0, 2.0, 3.0];

julia> B = [4.0, 5.0, 6.0];

julia> f.(pi, A)
3-element Array{Float64,1}:
 13.42477796076938
 17.42477796076938
 21.42477796076938

julia> f.(A, B)
3-element Array{Float64,1}:
 19.0
 26.0
 33.0

さらに、ネストされた f.(args...) の呼び出しは一つの broadcast ループに融合 (fuse) されます。例えば sin.(cos.(X))broadcast(x -> sin(cos(x)), X)と等価です。X に対するループは [sin(cos(x)) for x in X] と同様に一つだけであり、結果を収める配列だけがアロケートされます。これに対して典型的な "ベクトル化" 言語で sin(cos(X)) を計算すると、まず一次的な配列 tmp をアロケートして tmp=cos(X) を計算し、さらに別の配列をアロケートして sin(tmp) を計算するという処理が行われます。

ループの融合は起こったり起こらなかったりするコンパイラによる最適化ではなく、f.(args...) がネストされたときに必ず起こる構文的に保証された動作です。なお正確に言うと、融合は "ドットでない" 関数呼び出しが起こった時点で止まります。例えば sin.(sort(cos.(X))) では途中に sort があるために sincos のループを一つにすることはできません。

最後に、最大の性能はあらかじめアロケートされている配列にベクトル化演算を出力するときに達成されることがよくあります。つまり反復される関数の呼び出しが出力用の配列を何度も新しくアロケートしないということです (参照: 出力を事前にアロケートする)。このために便利な構文が X .= ... です。これは broadcast!(identity, X, ...) と等価であり、broadcast! のループは他のネストされたドット呼び出しと融合されます。例えば X .= sin.(Y)broadcast!(sin, X, Y) と等価であり、Xsin.(Y) で上書きされます。X[begin+1:end] .= sin.(Y) のように左辺が配列の一部であれば、その式は view に対する broadcast! に変換されます。つまり broadcast!(sin, view(X, firstindex(X)+1:lastindex(X)), Y) となり、左辺がインプレースに更新されます。

多くの演算子や関数にドットを付けるのは面倒でコードも読みにくくなるので、全ての関数呼び出し・演算子・代入をドットの付いたバージョンに変換する @. マクロが用意されています:

julia> Y = [1.0, 2.0, 3.0, 4.0];

julia> X = similar(Y); # 出力を事前にアロケートする

julia> @. X = sin(cos(Y)) # X .= sin.(cos.(Y)) と等価
4-element Array{Float64,1}:
  0.5143952585235492
 -0.4042391538522658
 -0.8360218615377305
 -0.6080830096407656

.+ などのドット付き二項演算子 (および単項演算子) も同じ規則で処理されます。つまりこういった演算子は broadcast の呼び出しと等価で、他のネストされたドット呼び出しと融合されます。例えば X .+= YX .= X .+ Y であり、融合されたインプレースの代入が行われます。ドット演算子の節にも説明があります。

ドット演算子は .|> で関数チェーンと組み合わせることができます。例を示します:

julia> [1:5;] .|> [x->x^2, inv, x->2*x, -, isodd]
5-element Array{Real,1}:
    1
    0.5
    6
   -4
 true

この次に読むべきマニュアル

この章の解説は Julia における関数定義の全体像のごく一部に過ぎないことをここで指摘しておくべきでしょう。Julia には洗練された型システムがあり、引数の型に応じた多重ディスパッチが可能です。この章で示した例は引数に対する型注釈を全く使っていませんが、これは関数が任意の型に対して適用できることを意味します。型システムはの章で説明され、実行時に引数の型に応じて多重ディスパッチによって選択されるメソッドとしての関数はメソッドの章で説明されます。

広告