制御構造

Julia は様々な制御構造構文を提供します:

最初の五つの制御構造は普通の高水準プログラミング言語で見られるものですが、タスクはそうではないでしょう: タスクはローカルでない制御構造を提供し、タスクを使うと計算を一時的に差し止めて別の計算に移る処理が可能になります。これは強力な構文であり、Julia では例外処理と協調的マルチタスクの両方がタスクを使って実装されています。日常的なプログラミングでタスクを直接使うことはないと思いますが、一部の問題はタスクを使うと非常に簡単に解決できます。

複合式

いくつかの式をまとめて評価する一つの式が書けると便利な場合があります。与えられた複数の式を順番に評価し、最後の式をその式全体の値とするということです。Julia にはこのための構文が二つ用意されています: begin ブロックと ; チェーンです。これらの複合式 (compound expression) の値は最後の部分式の値です。begin ブロックの例を示します:

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

この式は簡単で短いので、一行で書いた方が簡単かもしれません。一行で複合式を書くときは ; チェーンを使った構文が便利です:

julia> z = (x = 1; y = 2; x + y)
3

この構文は関数の章で説明した代入形式の関数定義で特に便利です。begin ブロックは複数行で、; チェーンは一行で書くのが普通ですが、そう決まっているわけではありません:

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

条件評価

条件評価 (conditional evaluation) を使うと、コードのある部分を評価するかしないかを真偽値型の式の値に応じて決めることができます。条件評価構文 if-elseif-else の使い方は次の通りです:

if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x is equal to y")
end

条件式 x < ytrue に評価されるなら、対応するブロック println("x is less than y") が評価されます。そうでなければ条件式 x > y が評価され、この値が true なら対応するブロック println("x is greater than y") が評価されます。二つの条件式がどちらも false であれば else ブロック println("x is equal to y") が評価されます。実行例は次の通りです:

julia> function test(x, y)
           if x < y
               println("x is less than y")
           elseif x > y
               println("x is greater than y")
           else
               println("x is equal to y")
           end
       end
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

elseif ブロックと else ブロックは省略でき、elseif ブロックはいくつでも並べることができます。if-elseif-else 構文の条件式は true に評価される最初の式まで評価が行われ、true に評価される最初の式に対応するブロックが評価されるとそれ以降の条件式とブロックは評価されません。

if ブロックは “穴空き” (leaky) です。言い換えると、if ブロックはローカルスコープを作りません。これは if 節の中で新しく定義された変数が if ブロックの後でも使えることを意味します。つまり上記の test 関数は次のようにも定義できます:

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           else
               relation = "greater than"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(2, 1)
x is greater than y.

コード中の変数 relationif ブロックの中で宣言され、if ブロックの外で使われています。ただしこの振る舞いを使うときは、可能なコードパス全てがその新しい変数の値を設定することを確認してください。上の関数を次のように変更すると実行時エラーとなります:

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(1,2)
x is less than y.

julia> test(2,1)
ERROR: UndefVarError: relation not defined
Stacktrace:
 [1] test(::Int64, ::Int64) at ./none:7

他の言語から来たユーザーは驚くかもしれませんが、if ブロックは値を返します。選択されたブランチで最後に実行された文の値がそのまま返り値となります:

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

Julia では非常に短い条件文 (ワンライナー) を短絡評価で書くことがよくあります。短絡評価は次の節で説明されます。

C, MATLAB, Perl, Python, Ruby とは異なり ──Java およびその他のいくつかの厳格な型付き言語と同様に── Julia では条件式が truefalse 以外の値を持つとエラーになります:

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

このエラーは条件式の型が間違っていることを示しています: 要求された型は Bool で、渡されたのは Int64 です。

「三項演算子 (ternary operator)」と呼ばれる ?:if-elseif-else とよく似ています。長いコードブロックを条件に応じて実行するのではなく、一つの式で表される値を条件に応じて選択するときに ?: は使われます。三項演算子と呼ばれるのは、多くの言語において三つのオペランドを取る演算子がこの演算子だけであるためです:

a ? b : c

? の前の式 a が条件式です。atrue なら : の前の式 b を評価し、afalse なら : の後ろの式 c を評価します。?: の周りのスペースは必須であり、a?b:c は正当な三項演算子の式ではありません (スペースは改行でも構いません)。

三項演算子の動作は例を見ると分かりやすいでしょう。これまでの例では全てのブランチで println が呼ばれており、実際に選択されているのは出力する文字列リテラルだけでした。このような場合に三項演算子を使うとコードが簡潔になります。簡単のため、まずは選択肢が二つのバージョンを示します:

julia> x = 1; y = 2;

julia> println(x < y ? "less than" : "not less than")
less than

julia> x = 1; y = 0;

julia> println(x < y ? "less than" : "not less than")
not less than

x < y が真だと三項演算子全体が文字列 "less than" に評価され、それ以外のときは文字列 "not less than" に評価されます。三つの選択肢がある元々の関数を書くには、三項演算子を連鎖させて使います:

julia> test(x, y) = println(x < y ? "x is less than y"    :
                            x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

三項演算子を連鎖させて書いたときに自然な振る舞いになるように、三項演算子は左から右に結合するようになっています。

if-elseif-else と同様に、: の前後にある式はそれぞれ条件式が truefalse になった場合にのみ評価されるというのは重要な事実です:

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"

julia> 1 > 2 ? v("yes") : v("no")
no
"no"

短絡評価

短絡評価 (short-circuit evaluation) は条件評価とよく似ています。Julia における短絡評価は真偽演算子 &&|| を持つ多くの命令型プログラミング言語と同様です: この二つの演算子でつながった真偽値型の式は、最終的な真偽値を決定するのに必要な分だけが評価されます。具体的には次の通りです:

afalse なら b の値に関わらず a && b は必ず false であり、同様に atrue なら b の値に関わらず a || b は必ず true である、という事実がこの振る舞いの理由です。&&|| はどちらも右結合ですが、&&|| よりも高い優先度を持ちます。この振る舞いは簡単に実験できます:

julia> t(x) = (println(x); true)
t (generic function with 1 method)

julia> f(x) = (println(x); false)
f (generic function with 1 method)

julia> t(1) && t(2)
1
2
true

julia> t(1) && f(2)
1
2
false

julia> f(1) && t(2)
1
false

julia> f(1) && f(2)
1
false

julia> t(1) || t(2)
1
true

julia> t(1) || f(2)
1
true

julia> f(1) || t(2)
1
2
true

julia> f(1) || f(2)
1
2
false

&& 演算子と || 演算子を混ぜたときの結合性や優先度も同様に実験できます。

Julia では非常に短い if 文を作るのに短絡評価が頻繁に使われます。if <cond> <statement> end の代わりに <cond> && <statement> と書き、if !<cond> <statement> end の代わりに <cond> || <statement> と書くということです。<cond> && <statement> は 「<cond> なら <statement>」という意味になり、<cond> || <statement> は「<cond> でなければ <statement>」という意味になります。

例えば、階乗を計算する再帰的なルーチンは次のように定義できます:

julia> function fact(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * fact(n-1)
       end
fact (generic function with 1 method)

julia> fact(5)
120

julia> fact(0)
1

julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fact(::Int64) at ./none:2
 [3] top-level scope

短絡評価を行わない真偽値演算にはビット演算子 &, | を使ってください。この演算子は中置記法をサポートする通常の関数に過ぎないので、引数を必ず評価します:

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

if, elseif, 三項演算子の条件式と同様に、&&|| のオペランドも真偽値 (true または false) である必要があります。ただし、連鎖された真偽演算子の最後のオペランドは真偽値でなくても構いません。それ以外の場所で真偽値型でない値を使うとエラーが発生します:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

連鎖された真偽演算子の最後の式は任意の型の値にできます。前方の条件式に応じて必要な場合にはその式が評価され、評価結果の値が返り値となります:

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

反復評価 (ループ)

式の反復評価には二つの構文が用意されています: while ループと for ループです。次に示すのは while ループの例です:

julia> i = 1;

julia> while i <= 5
           println(i)
           global i += 1
       end
1
2
3
4
5

while ループは条件節 (この例では i <= 5) を評価し、その評価結果が true ならループの本体を一度実行してから同じ処理をもう一度行います。条件節が最初から false であれば、本体は一度も実行されません。

for ループを使うとよく使われるループのパターンを簡単に書くことができます。上のコードのように while ループで変数を増加あるいは減少させる処理は非常によく現れるので、for ループを使って簡潔に表現できるようになっています:

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

ここで 1:5 は区間 (range) オブジェクトであり、1, 2, 3, 4, 5 という数列を表します。for ループはこれらの値をループ変数 i に代入しながら本体を反復します。

while を使ったループと for を使ったループの重要な違いが、変数が見えるスコープです。もし for を使ったループで i が外側のスコープに存在しなければ、i はループの内側でのみ見えるようになります。この事実を試すには新しい対話セッションのインスタンスまたは新しい変数の名前が必要です:

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

Julia における様々なスコープに関する詳しい説明は変数のスコープの章を参照してください。

一般に、for ループ構文では任意のコンテナの要素に関する反復が可能です。通常コンテナを使うときは = を等価なキーワード in または に変更します。for ... = ...= の代わりに in を使ってもコードの意味は変わりませんが、コードがより明快になります:

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s  ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

その他の反復可能なコンテナの型はマニュアルの後半で紹介・説明されます (多次元配列の章など)。

while ループを条件節が偽になる前に終了したり、for ループを反復可能オブジェクトの最後に到達する前に終了したりできると便利です。break キーワードを使うとこれを実現できます:

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           global i += 1
       end
1
2
3
4
5

julia> for j = 1:1000
           println(j)
           if j >= 5
               break
           end
       end
1
2
3
4
5

break キーワードを使わないと上記の while ループはいつまでも終了せず、for ループは 1000 まで反復します。しかし break があるので、両方のループは i5 になった時点で終了します。

反復を途中で止めて次の反復に移れると便利な状況もあります。continue キーワードを使うとこれを実現できます:

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

この例は少し不自然です: if の条件式を否定して continue の部分を println とすれば同じ処理が行えるためです。現実的なコードでは continue の後に長いコードが続き、continue が複数の場所から呼ばれます。

ネストされた複数の for ループは一つの for ループとして書くことができます。こうすると反復可能オブジェクトの直積に対する反復となります:

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

この構文では、内側のループで使う反復可能オブジェクトの構築で外側のループ変数を参照できます: for i = 1:n, j = 1:i として構いません。ただし、break すると二つのループからまとめて脱出します。また ij は内部のループが実行されるたびに新しい値に設定されるので、i への代入は次の反復へ伝わりません:

julia> for i = 1:2, j = 3:4
           println((i, j))
           i = 0
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

このコードでもし二つの変数それぞれに対して for ループを書いたとすれば、異なる出力が得られます: 二行目と四行目の第一要素が 0 となります。

例外処理

予期しない条件が成り立つと、関数から意味のある値を返せなくなることがあります。そのような場合に行えるのは、診断用のエラーメッセージを出力してプログラムを終了させるか、プログラマによって提供される例外的な状況に対応するコードを実行して適切な動作を行うかのどちらかです。

組み込みの例外

予期しない条件が成り立つと、例外 (Exception 型のインスタンス) が送出されます。次に示す組み込みの例外は全て通常の実行の流れを中断させます:

例えば sqrt に負の実数を与えると DomainError が発生します:

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

独自の例外を定義するには次のようにします:

julia> struct MyCustomException <: Exception end

throw 関数

例外は throw で明示的に送出できます。例えば非負実数に対して定義される関数は、引数が負のとき DomainErrorthrow するでしょう:

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
 [1] f(::Int64) at ./none:1

DomainError の後に括弧を付けないと例外にならないことに注意してください。DomainError が表すのは例外の型であり、Exception オブジェクトを作るには呼び出す必要があります:

julia> typeof(DomainError(nothing)) <: Exception
true

julia> typeof(DomainError) <: Exception
false

また、受け取った引数のいくつかをエラー報告に使う例外型もあります:

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

こういった機構は次のようにすれば簡単に実装できます。UndefVarError も同様の方法で実装されています:

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")
情報

エラーメッセージの最初の文字は小文字にすることを推奨します。例えば、次のような例外を書くべきです:

size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

次のような例外は書くべきではありません:

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B"))

ただし、大文字で始めたほうがよい状況もあります。例えば関数の引数が大文字である場合がそうです:

size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).

エラー

error 関数は ErrorException を生成し、通常の実行の流れを中断します。

例えば負の実数の平方根を取ろうとしたときに実行をすぐに中断したいとします。こうするには、引数が負のときエラーを送出する神経質fussyなバージョンの sqrt を定義します:

julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(2)
1.4142135623730951

julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt(::Int64) at ./none:1
 [3] top-level scope

fussy_sqrt が他の関数から負の値と共に呼ばれるとその時点で関数が返り、対話セッションにエラーメッセージが表示されます。呼び出し元の関数の実行が再開されることはありません:

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt at ./none:1 [inlined]
 [3] verbose_fussy_sqrt(::Int64) at ./none:3
 [4] top-level scope

try/catch

try/catch 文を使うと Exception の発生を検出でき、通常であればアプリケーションの実行を終了させる事象に対する適切な処理を行えます。例えば以下のコードでは sqrt 関数が例外を発生させますが、sqrt 関数の周りにある try/catch ブロックが例外に対処しています。例外の対処方法は catch の後に指定します: エラーを記録してとりあえず値を返すか、エラーメッセージを表示するかです。以下のコードでは後者としています:

julia> try
           sqrt("ten")
       catch
           println("You should have entered a numeric value")
       end
You should have entered a numeric value

予期せぬ状況の対処方法を考えるときは、try/catch が条件分岐よりはるかに遅いことを念頭に置いてください。この節は try/catch を使った例外処理の例をいくつか示します。

try/catch 文では Exception を変数に保存することもできます。次に示すのは、x が添え字に対応するなら x の第二要素の平方根を計算し、そうでなければ x が実数だと仮定してその平方根を計算する奇妙な関数です:

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

catch の後ろに付いたシンボルは必ず例外の名前として解釈されます。そのため try/catch を一行で書くときは注意が必要です。次のコードはエラーのときに x を返すコードとして正しくありません:

try bad() catch x end

catch の直後にセミコロンまたは改行を入れれば正しく動作します:

try bad() catch; x end

try bad()
catch
    x
end

try/catch の強みは深くネストされたコードからはるか上方にある呼び出し側のスタックまで一気に巻き戻れる点にあります。エラーが発生していなくても、スタックを撒き戻して値を渡す機能だけが必要となることもあるでしょう。Julia には rethrow, backtrace, catch_backtrace, Base.catch_stack といったさらに高度なエラー処理関数が用意されています。

finally

システム状態の変更やファイルといったリソースの更新を行うコードでは、コードの最後で後始末 (ファイルのクローズなど) が必要な場合がよくあります。例外があると通常のブロックの実行が終端に達する前に終わる可能性が生まれるので、後始末のタスクが複雑になります。このような場合に finally キーワードを使うと、あるブロックの実行が終了したときに必ず実行するコードを指定できます。

例えば、開いたファイルをクローズすること保証するには次のようにします:

f = open("file")
try
    # ファイル f を使った操作
finally
    close(f)
end

制御が try ブロックを (return または通常の実行終了によって) 離れるとき、close(f) が実行されます。例外によって try ブロックの実行が終了するときは、finally 節が実行された後に例外はさらに上に伝播します。catch ブロックで tryfinally を両方使うこともでき、この場合には finally ブロックは catch がエラーを処理した後に呼ばれます。

タスク (コルーチン)

タスク (task) は計算処理の柔軟な停止・再開を可能にする機能です。制御構造の一部なので一応ここで触れますが、詳しい説明は非同期プログラミングの章で行います。

日本語 Julia 書籍 (Amazon アソシエイト)
1 から始める Julia プログラミング
Julia プログラミングクックブック―言語仕様からデータ分析、機械学習、数値計算まで
スタンフォード ベクトル・行列からはじめる最適化数学