関数
Julia では、関数は引数値のタプルを受け取って、戻り値を返すオブジェクトです。Julia 関数は、グローバル変数の状態を変えたり、グローバル変数の状態に影響を受けたりするという点で、純粋な数学関数ではありません。関数を定義する基本的な構文は次のとおりです:
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
もっと簡潔に関数を定義する第二の構文があります。上記に示した従来の関数宣言構文と次のようなコンパクトな代入方式は同等の関数定義を行います:
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 関数の引数は、"共有渡し" と呼ばれる慣例に従います。関数に渡した引数はコピーされません。関数引数そのものは、新しい変数 バインディング (値を参照できる新しい場所) として振る舞いますが、参照する値は渡された値と同一のものです。ミュータブル(変更可能)な値(配列など)に対して、関数内で行われた変更は呼び出し側からも見えます。これは、Scheme、ほとんどのLisp 、Python、Ruby、Perlその他の動的言語で見られる動作と同じです。
return
キーワード
関数の戻り値は、最後に評価された式の値であり、デフォルトでは関数定義の本体の最後の式です。前節で挙げた 関数 f
の例では式 x + y
がこれに当たります。C や他のほとんどの命令型言語や関数型言語の大部分はが、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
をつかう意味はありません。x + y
という式は評価されず、単にx * y
を関数の最後の式にすれば return
を省略できるためです。ただし、他の制御フローと組み合わる場合、return
は実用的です。たとえば、オーバーフローを回避しながら x
と y
の辺を持つ直角三角形の斜辺の長さを計算する関数は下記のようになります:
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
この関数から、呼び出し側に戻るポイントは 3 箇所あり、x
と y
の値に応じて 3 つの異なる式の値を返します。最後の行の return
は最後の式であるため省略できます。
戻り値の型は、::
演算子を使用して関数宣言で指定することもできます。これにより、戻り値が指定された型に変換されます。
julia> function g(x, y)::Int8
return x * y
end;
julia> typeof(g(1, 2))
Int8
この関数は、x
と y
の型に関係なく、常に Int8
を返します。 戻り値の型の詳細については、型宣言を参照してください。
演算子は関数である
Julia では、ほとんどの演算子は特殊な構文をサポートする関数にすぎません。(例外は、&&
や ||
です。短絡評価は、演算子の評価が行われる前には、オペランドが評価されていないことを要求し、これは関数の振る舞いとは異なります) したがって、他の関数と同様に、括弧付き引数リストを使用して演算子を適用することもできます:
julia> 1 + 2 + 3
6
julia> +(1,2,3)
6
上記コードにおける1つ目の例の二項演算形式は、2つ目の例の関数適用形式と全く同等です。実は、前者は内部で関数呼び出しを行っているのです。これはまた、他の関数と同様に+
や*
]などの演算子は、代入や受け渡しが可能だということです:
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 では 関数はファーストクラスのオブジェクトです。:変数に代入ができ、代入された変数から標準的な関数呼び出し構文を使って関数を呼び出すことができます。これらは関数の引数としても、戻り値としても使うことができます。また、 下記の構文のいずれかを使用して、名前を付けず作成することができます:
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
の値を返す関数が作成されます。ここで生成される関数は、汎化関数ですが、コンパイラで生成した通し番号に基づいた名前を持つ点に注意してください。
無名関数の主な用途は、他の関数を引数に取る関数に渡すことです。典型的な例は[map
(@ref)]で、配列の各値に対して関数を適用し、その結果を新しい配列として返します:
julia> map(round, [1.2,3.5,1.7])
3-element Array{Float64,1}:
1.0
4.0
2.0
もし、変換を行う名前付きの関数があって、map
関数の第一引数に渡せるのであれば、問題は有りません。しかし、多くの場合、すぐに使用できる名前付き関数は存在しません。このような状況では、無名関数nの機能を使えば、その時限りの、名前を必要としない関数オブジェクトを簡単に生成することができます:
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
のように書かれます。引数のない関数の考えは奇妙に見えるかもしれませんが、計算を「遅らせる」ために役立ちます。この使用法では、コードブロックを後でゼロ引数関数で囲っておき、後でf
のように呼んで実行します。
タプル
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) タプルを表します。
名前付きタプル
タプルの各要素にはオプションで名前をつけることができます。このばあい、名前付きタプルが生成されます:
julia> x = (a=1, b=1+1)
(a = 1, b = 2)
julia> x.a
1
名前付きタプルはタプルに非常によく似ていますが、そのフィールドに対して、ドット構文 (x.a
) を使用して名前でアクセスできます。
複数の戻り値
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> range((min, max)) = max - min
julia> range(minmax(10, 2))
8
range
の定義で余計な括弧があるのに注目してください。これがなければ、range
は 2 引数関数になり、この例では機能しません。
可変引数関数
任意の個数の引数を取る関数を記述できると便利なことがよくあります。 このような関数は、伝統的に"varargs"関数として知られており、これは"variable number of arguments" (変数数の引数) の略です。varargs 関数を定義するには、最後の引数の後に、省略記号をつけると定義できます:
julia> bar(a,b,x...) = (a,b,x)
bar (generic function with 1 method)
変数 a
と b
は通常どおり最初の 2 つの引数の値にバインドされ、変数 x
は最初の 2 つの引数の後に bar
に渡された 0 個以上の値のイテラブルなコレクションにバインドされます:
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))
いずれの場合も、x
は bar
に渡された後続の値からなるタプルにバインドされます。
可変引数として渡される値の数を制限することが可能です。これについては、後で[パラメータ制限付きの可変引数メソッド](@ref parametrically-constrained-varargs-methods]で説明します。
また、多くの場合、イテラブル コレクションに含まれる値と関数呼び出しに個別の引数とを "接合" すると便利です。これを行うには、関数呼び出しで ...
を使用します:
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])
関数は、特定のy
年m
月d
日の Date
型を構成しますが、m
と d
引数は省略可能で、デフォルト値は 1
です。 この動作は、次のように簡潔に表すことができます:
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
見ての通り、この定義は、UTInstant{Day}
型の 1 つの引数を受け取る Date
関数の別のメソッドを呼び出します。
この定義によって,関数は 1 つ、2 つ、または 3 つの引数で呼び出され、引数のいずれかが指定されていない場合には 1
が自動的に渡されます:
julia> using Dates
julia> Date(2000, 12, 12)
2000-12-12
julia> Date(2000, 12)
2000-12-01
julia> Date(2000)
2000-01-01
オプション引数は、実際には、異なる数の引数を持つ複数のメソッド定義を記述するための便利な構文です (オプション引数・キーワード引数に関する注記を参照)。 これはmethods
関数を呼び出すことによって、例に挙げたDate
関数の例をチェックすることができます。
キーワード引数
関数の中には、引数の数が多いもの、多数の挙動を持つものがあり、このような関数の呼び出し方法を覚えておくのは難しいことがあります。キーワード引数を使用すると、引数を位置だけではなく名前で識別できるようになるので、これらの複雑なインターフェイスを使いやすくし、拡張も容易になります。
たとえば、線をプロットする関数 plot
を考えてみましょう。この関数には、線のスタイル、幅、色などを制御するための多くのオプションがあります。キーワード引数を受け入れる場合、可能な呼び出しは plot(x, y, width=2)
のようになり、線の太さのみを指定することができます。キーワード引数には2 つの目的があることに注意して下さい。引数に意味を付けることができるため、関数コールの可読性が上がります。また、多数の引数の任意の部分集合を任意の順序で渡すことができます。
キーワード引数を持つ関数は、シグネチャ内のセミコロンを使用して定義されます:
function plot(x, y; style="solid", width=1, color="black")
###
end
関数が呼び出す時に、セミコロンは省略可能です: 呼び出し方は、plot(x, y, width=2)
または plot(x, y; width=2)
ですが、前者ga より一般的です。以下に説明するように varargs または計算済みのキーワードを渡す場合には、明示的なセミコロンが必要になります。
キーワード引数のデフォルト値は、必要な場合 (対応する値が渡されずに)、必要になったときだけ、左から右の順番で評価されます。したがって、デフォルトの式は評価済みのキーワード引数を参照しても構いません。
キーワード引数の型は、次のように明示的に指定できます:
function f(;x::Int=1)
###
end
追加のキーワード引数はvarargs 関数のように ...
を使用して収集できます:
function f(x; y=0, kwargs...)
###
end
例の関数f
の内部では、kwargs
は名前付きタプル上でのkey-value イテレータとして処理されます。名前付きタプルは、(Symbol
をキーに持つ辞書と同じ様に) キーワード引数として関数にわたすことができて、呼び出時にはセミコロンを使います。(例: f(x、 z=1; kwargs...)
)
キーワード引数がメソッド定義にデフォルト値を割り当てられていない場合、その引数は必須ということになります。呼び出し元で値を割り当てなければ、UndefKeywordError
例外がスローされます:
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UndefKeywordError(:y)
セミコロンの後に key => value
の形式で渡すこともできます。たとえば、plot(x, y; :width => 2)
はplot(x, y, width=2)
と同等です。これは、実行時に、キーワード名が計算される場合に便利です。
キーワード引数の性質上、同じ引数を複数回指定することが可能です。 たとえば、plot(x, y; options..., width=2)
という呼び出しでは、options
構造体にも 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
構文は引数 x
を持つ無名関数を作成し、その関数を map
へ最初の引数として渡します。同様に、do a,b
は 2 引数の無名関数を作成し、単に do
と書けば、引数の無い () -> ...
という形の無名関数であることを宣言になります。
これらの引数がどのように初期化されるかは、外側の 関数によって異なります。ここで、map
は、構文 map(func,[A,B,C])
とした時と同様にx
をA
、B
、C
に順番に設定し、その都度無名関数が呼び出されます。
この構文を使用すると、関数呼び出しは通常のコード ブロックのように見えるため、簡単に、効果的に言語を拡張しやすくなります。システム状態の管理など、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
を外側のスコープから補足しています。キャプチャされます。キャプチャすると、パフォーマンス・ティップスで説明するように、パフォーマンス上の困難が生じる可能性があります。
関数をベクトル化するDot構文
技術計算向けのプログラミング言語では、関数の「ベクトル化」バージョンがあることが一般的です。これは所与の関数 f(x)
を配列 A
の各要素に適用するもので、f(A)
と書く新しい配列を生成します。この種の構文はデータ処理に便利ですが、他の言語では、パフォーマンスを得るためにも必要になることがあります。もし、ループ処理が遅ければ、「ベクトル化」バージョンの関数がより低水準な言語で書かれた速いライブラリーコードを呼ぶのです。Julia では、ベクター化された関数はパフォーマンスに必要なのではありません。実際に自分でループを書いた方がよいこともあります (パフォーマンス・ティップス参照)が、それでも、ベクトル化ができるのは便利です。そのような利便性のため、すべての Julia 関数 f
は、f.(A)
という構文で任意の配列 (またはその他のコレクション) に要素毎に関数を適用できます。 たとえば、ベクトル A
のすべての要素に sin
を以下のように適用できます:
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
の特殊な「ベクトル」版メソッド、例えばf(A:AbstractArray)= map(f,A)
を自作して使用すればドットを省略することもでき、演算効率も同じになります。しかし、この方法では、どの関数をベクトル化したいのか事前に決めておく必要があります。
より一般的には、f.(args...)
は実際には broadcast(f, args...)
に相当します。これは、複数のアレイ(異なる形状であってもよい)、または配列とスカラーの組み合わせを操作するものです。(ブロードキャストを参照)。たとえば、f(x,y) = 3x + 4y
を定義した場合、f.(pi,A)
は A
の各要素 a
に対する f(pi,a)
からなる新しい配列を返し、f.(vector1,vector2)
の場合は各インデックス f(vector1[i]、vector2)
からなる新しいベクトルを返します(ベクトルの長さが異なるときには例外を投げます)
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...)
の呼出は、単一のブロードキャストのループに融合されます。例えば、sin.(cos.(X))
は、broadcast(x->sin(cos(x)), X)
と等価で、[sin(cos(x)) for x in X]
に似ています。これは配列X
に対する一重のループになっており、計算結果の配列を一つだけアロケートします。[対象的に、通常のベクトル化した言語では、sin(cos(X))
とすると、まずはじめにtmp=cos(X)
に対してテンポラリの配列のためにメモリをアロケートし、その後、sin(tmp)
を別のループで計算、結果を格納する第二の配列を生成します。] Julia でのこのループ融合は、起こる場合も起こらない場合もあるような、コンパイルの最適化ではなく、ネストした f(args...)
呼出がある時に構文的に保証されているものです。技術的には、この融合は、「ドットを使わない関数呼び出し」に出会うと直ちにストップします。例えば、sin(sort(cos(X)))
では、sin
と cos
のループはsort
関数を挟んでいるため、融合されません。
最後に、演算効率が最大となるのは、通常、ベクトル化した操作の出力配列が 事前に確保されており、関数を何度も呼び出す度に、新しい出力用の配列をアロケートしなくてもよいときです。(出力の事前割当 参照)。これを行うための便利な構文が、X .= ...
です。これは broadcast!(identity, X, ...)
と等価ですが、broadcast!
ループはどんなにネストされた"dot" 呼び出しとも融合される点だけ異なります。たとえば、X .= sin
は、broadcast!(sin, X, Y)
と等価で、X
をsin(Y)
で上書きします。左辺が配列インデックス式である場合、例えばX[2:end] .= sin.(Y)
は、view
に対するbroadcast!
に変換されます。例えば、 broadcast!(sin, view(X, 2:lastindex(X))、Y)
で、左辺がインプレースで更新されます。
たくさんの演算子や関数の呼び出しにドットをつけると式が長ったらしくなり、可読性も低下するため、 @.
マクロが提供されています。これを使うと、行内の全ての関数よびだし、演算子、代入が「ドット付き」バージョンになります。
julia> Y = [1.0, 2.0, 3.0, 4.0];
julia> X = similar(Y); # pre-allocate output array
julia> @. X = sin(cos(Y)) # equivalent to X .= sin.(cos.(Y))
4-element Array{Float64,1}:
0.5143952585235492
-0.4042391538522658
-0.8360218615377305
-0.6080830096407656
.+
のような二項 (もしくは単項)演算子は、同じ仕組みで扱われます。これは broadcast
呼出と同等で、他のネストした「ドット」呼び出しと融合します。X .+= Y
は X .= 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 は洗練された型システムを持ち、引数型に対して多重ディスパッチ利用可能です。本節で示した例はいずれも、引数に対する型アノテーションをつけておらず、すべての型の引数に適用できます。型システムについては型セクションで説明されています。実行時引数の型による多重ディスパッチを行い、そこで選択されるメソッドに関して関数を定義する方法についてはメソッドで説明されています。