12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Julia 1.5 スタイルガイドの和訳

Last updated at Posted at 2020-10-12

Julia 1.5 Documentation の 39 章,Style Guide を和訳した.ただし Julia 自体には詳しくないので意味の取り違えがあるかもしれない.

Python のスタイルガイドである PEP 8 に比べると,コードの見栄えに関する言及は少なく,むしろアンチパターンの回避や実行性能を損ねない書き方についての説明が多い.


スタイルガイド

ここでは Julia らしいコーディングスタイルを説明する.どの規則も絶対的なものではなく,単なる提言であるが,Julia に慣れ親しむことや書き方を選択することにおいて役立つだろう.

スクリプトだけではなく関数を書こう

一連の処理をトップレベルに書き並べるというのは手軽に始められる方法だが,できるだけ早いうちから,プログラムをいくつかの関数に分けることを目指してほしい.関数を使う方が再利用やテストが楽にできるし,何の処理が行われるか・入力と出力が何であるかが明確になる.しかも,Julia のコンパイラの仕組みからすると,関数の内側のコードはトップレベルのコードより格段に速く動作することが多い.

ついでに言っておくと,関数はグローバル変数を直接いじるのではなく引数を取るようにすべきだ(pi などの定数であれば話は別だが).

型を必要以上に限定しない

コードはなるべく総称的である方がいい.次のような書き方をせず:

Complex{Float64}(x)

総称的な関数があればそれを使う方がいい:

Complex(float(x))

2 番目の例は,x を常に同じ型に変換するのではなく,適切な型を選んで変換してくれる.

この話は関数の引数との関連が深い.例えば,抽象型の Integer に当てはまる整数なら何でもいいという引数について,IntInt32 を宣言するのはよくない.実のところ,他のメソッド定義との区別が必要な場合を除けば,普段は引数の型が無指定でもよく,どこかで要求した操作に対応していない型が渡ったときには MethodError が投げられることになる.(いわゆるダックタイピングである.)

例えば,引数に 1 を加えた値を返す関数 addone の定義として,以下の 4 つを考える:

addone(x::Int) = x + 1               # Int のみ
addone(x::Integer) = x + oneunit(x)  # 任意の整数型
addone(x::Number) = x + oneunit(x)   # 任意の数値型
addone(x) = x + oneunit(x)           # + と oneunit に対応している任意の型

最後のメソッド定義では,oneunitx と同じ型で 1 に相当する値を返すので,不要な型昇格を避けられる)と + 関数に対応している型ならば何でも扱える.ひとつ知っておいてほしいことは,一般化された addone(x) = x + oneunit(x) だけを定義したからといって,実行性能で不利は生じないということであり,Julia は特定の型に特化したバージョンを必要に応じて自動的にコンパイルしてくれるのである.例えば,初めて addone(12) を呼ぶとき,Julia は自動的に引数が x::Int であるとき専用の addone をコンパイルし,そのときには oneunit の呼び出しはインラインの 1 に置き換えられる.したがって,先ほどの例では,addone の定義のうち最後の 1 つだけ書けばよく,他の 3 つは余計なものでしかない.

引数の過剰な多様性は呼び出し元で処理する

このような関数ではなく:

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 は一貫してこの規約に従っていて,コピーと変更の 2 通りが用意された関数もあるし(sortsort! など),変更のみの関数もある(push!pop!slice! など).利便性のため,配列を書き換えるような関数でも,その配列を返り値としている場合が多い.

奇怪な Union を登場させない

Union{Function,AbstractString} のような型が現れるならば,それは設計が汚くなっていて改善の余地がある証拠だろう.

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

普通は,次のような配列を生成しても,あまりいいことはない.

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

この場合は Vector{Any}(undef, n) とする方がいい.コンパイラにとっても,個別に配列を使うところでアノテーションを記述するのは(a[i]::Int など),様々な可能性をひとつの型に詰め込もうとするよりもありがたい.

Julia の base/ に整合する命名規則を使う

  • モジュール名と型名は大文字で始まるキャメルケースにする:module SparseArraysstruct UnitRange

  • 関数名は小文字にして(maximumconvert),可読性を損ねないならば複数の単語は連結する(isequalhaskey).必要ならば,単語をアンダースコアで区切る.アンダースコアは,複数の概念の組み合わせを表すときに使ったり(remotecall_fetchfetch(remotecall(...)) の効率的な実装),修飾子として使ったりすることもある.

  • 簡潔であるのはよいことだが,略語は避けるべきで(indexinindxin のようにしない),ある単語を略すことにしていたかどうか・どのように略すことにしていたかを覚えておくのが面倒になるからである.

関数名が複数の単語になりそうであれば,その関数が複数の概念を含んでいないか,もっと小さい部品に分けることができないかと検討してみよう.

関数の引数は Julia Base と同様の順序にする

原則として,Base ライブラリでは,可能な限り関数の引数を下記の順序にしている:

  1. それ自身が関数であるような引数.関数を引数に取る場合は,それを最初の引数とすることにより,do ブロックを用いて複数行の無名関数を渡すことが可能となる.

  2. I/O ストリーム.最初の引数が IO オブジェクトであれば,その関数を sprint などの関数に渡せる(例えば sprint(show, x)).

  3. 変更される入力.例えば fill!(x, v) では,x は変更されるオブジェクトであり,x に挿入しようとしている値よりも前に登場している.

  4. .型を引数に取る関数では,返り値がその型になっていることが多い.parse(Int, "1") において,型はパース対象の文字列より前に来ている.このように型が最初に来る例は多く存在するのだが,read(io, String) という例では,ここで説明している順序に従って IO 引数が型より前に来ている.

  5. 変更されない入力fill!(x, v) において v は変更されない値であり,x より後ろに来ている.

  6. キー.連想配列においては,key-value ペアのうち key の方である.その他のインデックス付きコレクションにおいてはインデックスである.

  7. .連想配列においては,key-value ペアのうち value の方である.fill!(x, v) では v がこれに相当する.

  8. その他.他に該当しないあらゆる引数.

  9. 可変長引数.関数呼び出しの最後にいくらでも並べられる引数のこと.例えば Matrix{T}(undef, dims) では,行列の形状を 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] と書くべきであり,これだけで配列の連結は実現できる.collect(a)[a...] よりマシだが,a 自体が既に反復可能オブジェクトだから,配列に変換しないでそのまま使う方が得策となることが多い.

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

このような関数の表記は:

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

次のように書き直すべきだ:

foo(x::Real) = ...

特に,T が関数本体で使われていない場合には.T が出てくるとしても,代わりに typeof(x) を使う方が簡便なこともある.どちらでも実行性能に違いはない.なお,静的パラメータの使用を常に避けろと言っているのではなく,単に必要がなければ使うなということだ.

もうひとつ言っておくと,特にコンテナ型に関しては,関数呼び出しで型パラメータが必要になるかもしれない.詳しくは FAQ の「抽象コンテナ型のフィールドを使わない(Avoid fields with abstract containers)」を参照されたい.

それがインスタンスであるのか型であるのか明確にする

次のようなメソッド定義は混乱を招く:

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() のようなよく知られた型について,ユーザはある一定の挙動を期待するものであり,その挙動をいじりすぎると扱いにくくなってしまう.

型を私物化しない

「型の私物化」というのは,Base やその他のパッケージにあるメソッドを拡張あるいは再定義して,自作でない型に対応させる行為のことである.型を私物化しても,大した弊害を被らずに済んでしまうときもある.しかし,場合によっては,Julia がクラッシュすることすらあり得る(メソッドの拡張や再定義によって ccall に変な入力が渡されたときなど).型を私物化すると型推論がややこしくなることがあるし,互換性を損ねた上にその予測や原因検証が困難になることだってある.

例として,あるモジュールの中でシンボル同士の乗算を定義したいとしよう:

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

このとき,Base.* を使っている他のあらゆるモジュールからこの定義が見えてしまうのが問題となる.SymbolBase 上に定義されているので,無関係な他のコードの挙動が意図せず変わってしまうことがある.代替手段としては,異なる関数名を使う,Symbol を自作の型でラップするなどの手がある.

時には,機能を定義から分離するために,2 つのパッケージが型の私物化で結合していることがあるかもしれず,特に複数人で設計するときや定義が再利用可能であるときに起こりがちだ.例えば,色を扱うのに便利な型を用意しているパッケージがあるとして,色空間の変換のために,他のパッケージがその型に対応するメソッドを定義するしれない.あるいは,C 言語のコードのちょっとしたラッパーとして動作するパッケージがあるとして,他のパッケージがそれを私物化して,高レベルで Julia らしい API を実装しようとするかもしれない.

型の等価性を慎重に扱う

原則として,型をチェックするには == ではなく isa<: を使うべきだ.あなたが本当に,間違いなく自分が何をやっているかを理解しているのでない限りは,型の厳密な一致を調べることに意味があるとすれば,既知の具象型と比較するとき(T == Float64 など)だけだ.

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

見ての通り,2 番目の例は Int リテラルを使っていて,こちらは引数として渡された値の型が保持されているが,1 番目の例ではそうなっていない.これは,promote_type(Int, Float64) == Float64 などの関係に従い,乗算の際に型昇格が起きてしまうことが原因である.同様に,Rational リテラルを使った場合は,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} 型を使うことで,比較的使いやすいコードにできる.

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?