スタイルガイド

この章では Julia コードが従うべきコーディングスタイルを説明します。こういった規則は絶対的なものではなく、言語に慣れるため、そしていくつかある選択肢の中から選択するときに参考にするための提案にすぎません。

スクリプトではなく関数を書く

トップレベルで少しずつコードを書いて処理を行う方法は問題解決の最初の一歩として悪くありませんが、なるべく早くプログラムを関数に分割できないかを考えるべきです。関数はスクリプトより再利用しやすく、テストしやすく、入力と出力に対して何を行うのかが明確です。さらに Julia コンパイラの動作方法の関係で、関数内部のコードはトップレベルのコードよりずっと速く実行できます。

また関数ではグローバル変数に直接処理を行うのではなく、引数として受け取ったグローバル変数に処理を行うべきであることも強調に値します (例外は pi のような定数です)。

不必要に特定的な型を使わない

コードは可能な限り汎用的であるべきです。例えば、次のように書かないでください:

Complex{Float64}(x)

Julia が提供する総称関数を使った方が優れたコードになります:

complex(float(x))

一つ目のコードは必ず Complex{Float64} 型の値を生成しますが、二つ目のコードでは x の型に応じて型が切り替わります。

このスタイルは特に関数の引数で重要です。例えば、任意の整数を取れる引数の型は IntInt32 ではなく抽象型 Integer と宣言してください。実を言えば、他のメソッド定義と区別するのでない限り、多くの場合で引数の型は省略できます: 必要な操作をサポートしない型が渡されると MethodError が発生するからです (これはダックタイピングと呼ばれます)。

例えば、引数に 1 を足す addone 関数に対する次の定義を考えます:

addone(x::Int) = x + 1              # Int だけ
addone(x::Integer) = x + oneunit(x) # 任意の整数型
addone(x::Number) = x + oneunit(x)  # 任意の数値型
addone(x) = x + oneunit(x)          # + と oneunit をサポートする任意の型

最後の addone の定義は oneunit (x と同じ型の 1 を返す、不必要な型の昇格を避けるための関数) と + をサポートする任意の型を処理できます。ここで注目すべき事実は、Julia は必要に応じて特殊化したコードを自動的にコンパイルするので、汎用的な addone(x) = x + oneunit(x) という定義を使っても性能の低下が起こらないことです。例えば addone(12) を初めて呼び出すと、Julia は自動的に addone 関数を x::Int として特殊化した関数をコンパイルし、そのとき oneunit(1) はインライン化されて 1 となります。そのため最初の三つの定義は四つ目の定義に完全に含まれると言えます。

引数の型の調整は呼び出し側で行う

次のように書かないでください:

function foo(x, y)
    x = Int(x); y = Int(y)
    ...
end
foo(x, y)

次のように書いてください:

function foo(x::Int, y::Int)
    ...
end
foo(Int(x), Int(y))

後者のスタイルが優れている理由は、foo が実際に受け取れるのは任意の型の数値ではなく Int だけであるためです。

ここで念頭にあるのが、関数が本質的に整数を必要とするなら、整数でない値の変換 (切り捨て/切り上げ/四捨五入) を呼び出し側に任せた方が良いだろうという考えです。また特定的な型で関数を定義しておけば、将来メソッドを追加することもできます。

引数を変更する関数では名前の最後に ! を付ける

次のように書かないでください:

function double(a::AbstractArray{<:Number})
    for i = firstindex(a):lastindex(a)
        a[i] *= 2
    end
    return a
end

次のように書いてください:

function double!(a::AbstractArray{<:Number})
    for i = firstindex(a):lastindex(a)
        a[i] *= 2
    end
    return a
end

Julia Base はこの慣習を全ての関数で使っており、sortsort! のようにコピーを返すバージョンと改変を行うバージョンの両方を持つ関数もあれば、push!, pop!, splice! のように更新を行うバージョンだけを持つ関数もあります。利便性のため、更新を行う関数でも更新された値を返すのが一般的です。

奇妙な Union を使わない

Union{Function,AbstractString} のような型は設計に改善の余地があるサインであることがよくあります。

手の込んだコンテナ型を使わない

次のような配列を作る意味はほとんどありません:

a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)

この場合は単に Vector{Any}(undef, n) としてください。現れる可能性のある型を全て一つの配列に詰め込むよりも、配列を使うときに型注釈 (a[i]::Int など) を付ける方がコンパイラの役に立ちます。

Julia Base と同様の命名規則を使う

自然に付けた関数の名前が複数の語からなるなら、その関数に分割すべき複数の処理が含まれていないかを考えてください。

引数は Julia Base と同様の順序で並べる

一般的な規則として、Base ライブラリでは関数の引数を並べるときに次の順序を (適宜) 使っています:

  1. 関数引数: 引数に関数を受け取るときは、それを第一引数とすると do ブロックを使って複数行に渡る無名関数を渡せるようになります。

  2. IO ストリーム: IO オブジェクトを関数の第一引数とすると、その関数は sprint などの関数に渡せるようになります (sprint(show, x) など)。

  3. 変更される入力: 例えば fill!(x, v) では x が変更される入力であり、挿入される値 v よりも前に表れています。

  4. : 関数に型を渡すときは通常、出力がその型になることを意味します。例えば parse(Int, "1") では型がパースする文字列の前に来ています。型が最初に来る関数は多くありますが、注目に値する例外は read(io, String) です。この関数では IO オブジェクトが型より先に来ており、ここで説明される順序に従っています。

  5. 変更されない引数: fill!(x, v) では v が変更されない引数であり、x の後に来ています。

  6. キー: 連想コレクションではキーバリューペアのキー、他のコンテナでは添え字のことです。

  7. : 連想コレクションではキーバリューペアのバリュー、fill!(x, v) のような場合には v のことです。

  8. その他: これ以外の全ての引数です。

  9. 可変長引数 (varargs): これは関数呼び出しの最後に好きな個数だけ付けられる引数です。例えば Matrix{T}(undef, dims) の次元 dimsTuple または Vararg で指定でき、それぞれ (Matrix{T}(undef, (1,2))) および Matrix{T}(undef, 1, 2) のように使います。

  10. キーワード引数: Julia ではキーワード引数を必ず関数定義の最後に付けます。ここに挙げられているのは完全性のためです。

ここに示した全ての種類の引数を取る関数はほとんど無いはずです。数字は優先度を表しているので、それを使って関数ごとに引数の順番を判断してください。

もちろん例外もあります。例えば convert では型が最初に来ます。また setindex! では値が最初に来て、添え字を可変長引数として渡せるようになっています。

API を設計するときは、可能な限りこの一般的な順序に従うことでユーザーに一貫した体験を提供できるでしょう。

try-catch を使い過ぎない

エラーを出して捕捉するよりは、最初からエラーを出さない方が優れています。

条件文に括弧を付けない

Julia では ifwhile の条件文に括弧が必要ありません。次のように書いてください:

if a == b

次のようには書かないでください:

if (a == b)

... を使い過ぎない

... を使った配列のスプライシングを使いまくる人がいます。[a..., b...] と書かなくても、単に [a; b] と書けば配列の連結を行えます。また a[...] よりも collect(a) が優れています。さらに a は最初から反復可能オブジェクトなので、おそらく配列に変換しないでそのまま使った方がよいでしょう。

不必要な静的パラメータを使わない

次の関数シグネチャは望ましくありません:

foo(x::T) where {T<:Real} = ...

次のように書いてください:

foo(x::Real) = ...

特に T が関数本体に表れない場合は必ずこうするようにしてください。T が使われていたとしても、typeof(x) が利用できるなら型パラメータは必要ありません。なお、こうしても性能は変わりません: これは型パラメータの利用に関する一般的な注意事項であり、使わなくて済む場合は使うべきでないというだけです。

コンテナ型を受け取る関数では型パラメータが必要になることがあります。詳細はパフォーマンス Tips のフィールドに抽象コンテナを使わないを見てください。

変数の型とインスタンスを混同しない

次のようなメソッドがどちらも定義されていると、理解が難しくなります:

foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)

考えている概念が MyTypeMyType() のどちらとして表されるべきかを判断し、どちらか一方だけを使ってください。

推奨されるスタイルは最初インスタンスを使い、問題の解決に必要になった段階で Type{MyType} のメソッドを追加するというものです。

ある型が事実上の列挙型なら、それは単一の型 (理想的には不変構造体またはプリミティブ型) として定義され、列挙型の値がそのインスタンスとなるべきです。そうした上でコンストラクタと変換処理で値が有効かどうかを確認してください。列挙型を抽象型にして "値" を部分型にする設計よりも、この単一の型を使った設計が推奨されます。

マクロを使い過ぎない

マクロを関数として書けないかどうかを意識するようにしてください。

マクロに含まれる eval の呼び出しは特に危険な警告とみなすべきです。これはそのマクロがトップレベルでしか動作しないことを意味します。そのようなマクロを関数として書けば、必要な実行時の値へ自然にアクセスできるでしょう。

安全でない処理をインターフェースのレベルに公開しない

ネイティブポインタを使う次の型があるとします:

mutable struct NativeType
    p::Ptr{UInt8}
    ...
end

このとき、次のような関数を定義しないでください:

getindex(x::NativeType, i) = unsafe_load(x.p, i)

この関数の問題は、ユーザーが安全でないことを意識することなく x[i] と書けてしまうことです。これは容易にメモリ関連のバグに結びつきます。

こういった関数には安全性を確認する処理を入れるか、あるいは名前に unsafe を入れて呼び出し側に注意を促すべきです。

基礎的なコンテナ型のメソッドをオーバーロードしない

次のようなメソッド定義の追加は可能です:

show(io::IO, v::Vector{MyType}) = ...

こうすれば特定の新しい要素型を持つベクトルを独自の方法で表示できます。こうしたくなるかもしれませんが、これは行うべきではありません。ユーザーは Vector といったよく知られた型に対しては通常通りの振る舞いを期待するので、この振る舞いをカスタマイズしすぎるとユーザーにとって扱いにくい型となってしまいます。

「型の強奪」を避ける

「型の強奪 (type piracy)」とは、自分が定義していない型に対して Base など他のパッケージのメソッドを拡張・再定義することを言います。型を強奪しても何も起こらないこともありますが、最悪の場合には Julia がクラッシュします (例えばメソッドの拡張や再定義によって ccall に不正な値が渡されたときなど)。型の強奪があるとコードの読解が困難になり、予測や診断が難しい非互換性が生まれる可能性もあります。

例えば、あなたが書いているモジュールでシンボル同士の乗算を定義したいとします:

module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end

ここでの問題は、Base.* を使う他の全てのモジュールからもこの定義が見えることです。Symbol は Base で定義され他のモジュールも Symbol を利用するので、モジュール A の新しい定義により無関係なコードの振る舞いが知らないうちに変わるかもしれません。代替手段としては、異なる名前の関数とする、Symbol を新しい型で包むといった方法があります。

複数の関連するパッケージが型と機能を分離するためにあえて型の強奪を行っている場合もあります。そういったパッケージはたいてい同じ著者によって設計され、型だけの再利用が意図されています。例えば、あるパッケージが色を扱うのに便利な型を提供し、他のパッケージがその型を使った色空間の間の変換メソッドを提供するといった状況です。あるいは、一つのパッケージが C コードの薄いラッパーとなる型を提供し、Julia フレンドリーな高水準の API を実装する別のパッケージがその型を強奪するという状況もあり得ます。

型の等価判定に注意する

型の比較には基本的に isa<: を利用し、== は使わないでください。型の正確な等価判定が意味を持つのは既知の具象型と比較しているとき (例えば T == Float64)、および自分が何をしているかをあなたが本当に、心の底から理解しているときだけです。

x->f(x) と書かない

高階関数には無名関数が渡されることが多いので、x->f(x) という形が望ましい、あるいは必要であると思ってしまうかもしれません。しかし高階関数には任意の関数を無名関数に包まずにそのまま渡せます。map(x->f(x), a) と書かずに map(f, a) と書いてください。

可能なら、汎用なコードに浮動小数点数リテラルを書かない

数値を処理する汎用なコードを書いていて、異なる数値型を引数に取ることを予定しているなら、型の昇格を通して引数の型を変えてしまう数値型を持ったリテラルを取り除いてください。

例を示します:

julia> f(x) = 2.0 * x
f (generic function with 1 method)

julia> f(1//2)
1.0

julia> f(1/2)
1.0

julia> f(1)
2.0

この一方で、次の実行結果が成り立ちます:

julia> g(x) = 2 * x
g (generic function with 1 method)

julia> g(1//2)
1//1

julia> g(1/2)
1.0

julia> g(1)
2

ここから分かるように、Float64 型のリテラルを使う一つ目のバージョンでは引数の型と返り値の型が一致しないのに対して、Int 型のリテラルを使う二番目のバージョンでは引数の型が一致します。この理由は promote_type(Int, Float64) == Float64 といった関係が成り立ち、乗算で型の昇格が起こっているためです。同様に Rational(Int) 型のリテラルを使えば型の破壊が Float64 型のリテラルよりは少なくなりますが、Int 型のリテラルよりは多くなります:

julia> h(x) = 2//1 * x
h (generic function with 1 method)

julia> h(1//2)
1//1

julia> h(1/2)
1.0

julia> h(1)
2//1

このため、可能な場合には Int リテラルを使用し、整数でないリテラルには Rational{Int} 型を使ってください。こうするとコードは引数の型を変えたときにも使いやすくなります。

広告