関数
Juliaでは、関数とは引数の値のタプルに対して戻り値を返すオブジェクトです。 Juliaの関数は純粋に数学的な関数では、ありません。 これは、プログラムのグローバルな状態によって変更されたり、影響を受けたりするという意味です。 Juliaで関数を定義する基本構文は以下の様になります。
julia> function f(x,y)
x + y
end
f (generic function with 1 method)
Juliaには、第二の簡潔な関数定義の構文があります。 上記の従来の関数宣言の構文と下記のコンパクトな"代入形式"は同等です。
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
変数と同じように関数名にはユニコードを利用可能です。
julia> ∑(x,y) = x + y
∑ (generic function with 1 method)
julia> ∑(2, 3)
5
引数引き渡しの挙動
Juliaの引数は"共有渡し"と呼ばれる慣例に従ています。 これは、関数に渡すときに複写をしないという意味です。 関数の引数自体は新しい変数 束縛 のように振る舞いますが、値は受け取る値と同じものを参照しています。 (配列のような)可変な値を関数内で変更すると、呼び出し側からも見えます。 こうした挙動は、Scheme、ほとんどのLisp、Python、Ruby、Perlその他の動的言語でみられるものと同じです。
[](## The
return` Keyword)
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
のように順次処理だけを行う関数の本体で、ruturn
という用法をつかっても意味がありません。 というのも、式x + y
は決して評価されず、単にx * y
を関数内の最後の式にしてreturn
を省いてもいいからです。 しかし他の制御フローとつなぎ合わせる場合、return
は実用的です。 直角を挟む2辺が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箇所、終了しうる場所があり、3つの異なる式の値を返しますが、x
とy
の値に依存します。 最終行の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
中置形式は関数適用形式と全く同等です。 実のところ前者は内部で関数呼び出しを行っています。 これは、+
や *
といった演算子に対して、他の関数とおなじように、代入や受け渡しが可能だと言うことです。
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
で、この関数は、配列の各値に対して関数を適用し、その結果を新しい配列として返します。
julia> map(round, [1.2,3.5,1.7])
3-element Array{Float64,1}:
1.0
4.0
2.0
これは、変換を行う名前付き関数がすでに存在し、 map
の第1引数に渡せる場合は申し分ありません。 しかし、すぐに使える既存の関数がないことは、よくあります。 そんな時は、無名関数の構文で、名前を必要としない使い捨ての関数オブジェクトを簡単に作ることができます。
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には、組込みの タプル と呼ばれるデータ型があり、関数の引数や戻り値と密接に関係しています。 タプルは長さの決まったコンテナで、任意の値を保持しますが、変更はできません。(つまり 不変 です)。 タプルはコンマと括弧で構成され、インデックスを使ってアクセスできます。
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
Juliaでは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個の関数となり、この例はうまく動作しません。
可変引数関数
引数の数が任意個の関数が書けると便利なことがよくあります。 そういった関数は従来、”可変引数”関数として知られ、これは”可変個の引数”の略です。 可変引数関数は、最後の引数のあとに省略記号をつけると定義できます。
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
に渡された後続の値からなるタプルに束縛されます。
可変引数として渡される値の個数を制限することも可能ます。 これは後述のパラメータ制限つきの可変引数メソッドで議論します。
また、イテラブルコレクションに含まれる値と、関数呼び出しの各引数とは、簡単に"接合"できるので、よく使われます。
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
見ての通り、この定義ではDate
関数の別メソッドで、UTInstant{Day}
型の1個引数にとるものを呼び出しています。 この定義によって、関数は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)
ですが、前者のほうがよく使われます。 明示的にセミコロンを使う必要があるのは、下記のような、可変引数を渡す場合か計算結果のキーワードを渡すときです。
キーワード引数のデフォルト値は必要な時だけ左から右へ評価されます(対応するキーワード引数が渡されないときです)。 そのため、デフォルト式は先にでたのキーワード参照可能です。
function f(;x::Int=1)
###
end
余分のキーワード引数は、可変引数関数を同じように...
を使ってひとまとめにすることができます。
function f(x; y=0, kwargs...)
###
end
キーワード引数がメソッドの定義でデフォルト値を設定されていない場合は、 入力必須 となります。 呼び出し側が値を代入しない時には、 UndefKeywordError
が投げられます。
function f(x; y)
###
end
f(3, y=5) # ok, y is assigned
f(3) # throws UndefKeywordError(:y)
f
の内部でkwargs
は名前付きタプルになります。 名前付きタプルは(辞書と同じように)、キーワード引数として関数に渡すことができて、呼び出す時にセミコロンを使います。 f(x, z=1; kwargs...)
のように。 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
に1番目の引数として渡します。 同様に、do a,b
では、引数が2個の無名関数を生成します。 また、単にdo
と書いた場合、do
に続く記述を ...
とすると、 その記述が() -> ...
という形の無名関数だという宣言になります。
これらの引数がどのように初期化されるかは、"外側"の関数に依存します。 ここでは、map
は、x
に A
、B
、C
を順に代入しから、無名関数を呼び出して、map(func, [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
で定義した無名関数の演算結果を出力ストリームに渡します。 関数を終了したあとは、ストリームが適切に終了したかを確認します。 これは、正常終了の場合も、例外を投げた場合も同様です。 (try/finally
構文については、制御フローで記述します)
do
ブロック構文に関しては、ユーザー関数の引数が同初期化されるのかを知るには、ドキュメントや実装を確認すると役に立ちます。
do
ブロックは、他の内部関数と同様に、取り囲むスコープの変数を"捕捉"することができます。 例えば、上述のopen...do
の中の変数data
は、外側のスコープから補足しています。 変数を捕捉すると、 パフォーマンス・ティップスで議論するように、パフォーマンス上の困難が生じる可能性があります。
関数をベクトル化するDot構文
技術計算向けのプログラム言語では、関数の"ベクトル化"版があることが多く、これは所与のf(x)
を、配列A
の各要素に適用して、f(A)
と書かれる新しい配列を生成するものです。
この種の構文は、データ処理に便利ですが、他の言語では、パフォーマンスのためによく必要になります。 ループが遅い場合に、"ベクトル化"版の関数を、低級言語で書かれた速いライブラリから呼びます。 Juliaでは、パフォーマンスのためには、”ベクトル化”版の関数は必要なく、それどころか、自分でループ書いた方がいいことがよくありますが (パフォーマンス・ティップス参照)、それでも便利です。 そのため、すべての Juliaの関数 f
は、f.(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
のメソッドを 、f(A::AbstractArray) = map(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)
の場合は、各インデックス 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...)
の呼び出しは単一の ブロードキャスト
のループに 融合 します。 例えば、sin.(cos.(X))
はbroadcast(x -> sin(cos(x)), X)
と同等で、[sin(cos(x)) for x in X]
に似ています。 これはx
に関する一重のループで、配列1個を結果に割り当てます。 [対照的に、sin(cos(X))
は通常の"ベクトル化した"言語では、まず一時的な配列をtmp=cos(X)
に割り当て、次にsin(tmp)
を別のループで計算して第2の配列を割り当てます] このループ融合は、コンパイラの最適化(おこる場合もおこらない場合もある)ではなく、ネストしたf.(args...)
呼び出しがある時に 構文的に保証されているものです。 技術的には、融合は"ドットをつかわない"関数呼び出しに出会うと直ちに停止します。 例えば、sin.(sort(cos.(X)))
ではsin
と cos
のループは、sort
関数を挟んでいるために、融合できません。
最後に、効率が最大となるのは、通常、ベクトル化した操作の出力が 事前に割り当てられているときで、関数を何度も呼び出すたびに、新しい配列の割り当てが何度もおこるのを防ぐためです。(出力の事前割り当てを参照) このための簡単な構文は、X .= ...
で、これは broadcast!(identity, X, ...)
とほぼ同等ですが、上記のように broadcast!
ループは、どんなにネストした"ドット"呼び出しとも融合する点が違います。 例えば、X .= sin.(Y)
は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には洗練された型システムがあり、引数の型に対する多重ディスパッチが利用可能です。 このセクションでの例には引数に全く型注釈をつけていません。 これは、すべての型を適用可能だということを意味します。 型システムの記述は型にあり、 関数の定義にメソッドを使い、実行時の引数の型に多重ディスパッチで選択する記述はMethodsにあります。