Style Guide

以下のセクションでは、慣用的なJuliaコーディングスタイルのいくつかの側面について説明します。これらのルールは絶対的なものではなく、言語に慣れる手助けや、代替設計の中から選択する際の参考としての提案に過ぎません。

Indentation

4つのスペースをインデントレベルとして使用します。

Write functions, not just scripts

コードをトップレベルで一連のステップとして書くことは、問題解決を始めるための迅速な方法ですが、できるだけ早くプログラムを関数に分割するように努めるべきです。関数は再利用可能でテスト可能であり、どのステップが実行されているのか、またその入力と出力が何であるかを明確にします。さらに、関数内のコードは、Juliaのコンパイラの動作により、トップレベルのコードよりもはるかに速く実行される傾向があります。

関数は引数を受け取るべきであり、グローバル変数(piのような定数を除いて)に直接作用すべきではないことも強調する価値があります。

Avoid writing overly-specific types

コードはできるだけ一般的であるべきです。次のように書くのではなく:

Complex{Float64}(x)

利用可能な汎用関数を使用する方が良いです:

complex(float(x))

2番目のバージョンでは、常に同じ型ではなく、xを適切な型に変換します。

このスタイルポイントは、特に関数の引数に関連しています。例えば、引数を型 Int または Int32 として宣言しないでください。本当に任意の整数である場合は、抽象型 Integer で表現されます。実際、多くの場合、他のメソッド定義と区別するために必要でない限り、引数の型を完全に省略することができます。なぜなら、型が必要な操作をサポートしていない場合は、MethodError が投げられるからです。(これは duck typing として知られています。)

例えば、引数に1を加えた値を返す関数 addone の以下の定義を考えてみましょう:

addone(x::Int) = x + 1                 # works only for Int
addone(x::Integer) = x + oneunit(x)    # any integer type
addone(x::Number) = x + oneunit(x)     # any numeric type
addone(x) = x + oneunit(x)             # any type supporting + and oneunit

addoneの最後の定義は、oneunitをサポートする任意の型を処理します(これは、xと同じ型で1を返し、不要な型の昇格を回避します)およびその引数を持つ+関数です。重要な点は、一般的なaddone(x) = x + oneunit(x)のみを定義することに パフォーマンスペナルティはない ということです。なぜなら、Juliaは必要に応じて自動的に特化したバージョンをコンパイルするからです。たとえば、最初にaddone(12)を呼び出すと、Juliaは自動的にx::Int引数用の特化したaddone関数をコンパイルし、oneunitへの呼び出しはそのインライン値1に置き換えられます。したがって、上記のaddoneの最初の3つの定義は、4番目の定義と完全に冗長です。

Handle excess argument diversity in the caller

その代わりに:

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 が必要だからです。

1つの問題は、関数が本質的に整数を必要とする場合、呼び出し元に非整数をどのように変換するか(例:切り捨てまたは切り上げ)を決定させる方が良いかもしれないということです。もう1つの問題は、より具体的な型を宣言することで、将来のメソッド定義のための「スペース」が増えるということです。

Append ! to names of functions that modify their arguments

その代わりに:

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

使用:

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

Julia Baseはこの規則を通じて使用されており、コピー形式と修正形式の両方の関数の例が含まれています(例:sortおよびsort!)、および単に修正するもの(例:push!pop!splice!)。 このような関数は、便利さのために修正された配列を返すことが一般的です。

IOや乱数生成器(RNG)を利用する関数は特筆すべき例外です:これらの関数はほぼ必ずIOやRNGを変化させなければならないため、!で終わる関数は、IOを変化させることやRNGの状態を進めること以外の変化を示すために使用されます。例えば、rand(x)はRNGを変化させますが、rand!(x)はRNGとxの両方を変化させます。同様に、read(io)ioを変化させますが、read!(io, x)は両方の引数を変化させます。

Avoid strange type Unions

Union{Function,AbstractString}のような型は、しばしば設計がよりクリーンであるべきであることを示すサインです。

Avoid elaborate container types

通常、以下のような配列を構築することはあまり役に立ちません:

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

この場合、Vector{Any}(undef, n) の方が良いです。また、特定の使用法を注釈すること(例:a[i]::Int)は、1つの型に多くの代替案を詰め込もうとするよりも、コンパイラにとってもより役立ちます。

Prefer exported methods over direct field access

イディオマティックなJuliaコードは、一般的にモジュールのエクスポートされたメソッドをその型へのインターフェースとして扱うべきです。オブジェクトのフィールドは一般的に実装の詳細と見なされ、ユーザーコードはこれがAPIであると明記されている場合にのみ直接アクセスすべきです。これにはいくつかの利点があります:

  • パッケージ開発者は、ユーザーコードを壊すことなく実装を変更する自由があります。
  • メソッドは、mapのような高階構造に渡すことができます(例えば、map(imag, zs)の代わりに[z.im for z in zs])。
  • メソッドは抽象型に定義することができます。
  • メソッドは、異なるタイプ間で共有できる概念的な操作を説明できます(例:real(z)は複素数や四元数で機能します)。

Juliaのディスパッチシステムはこのスタイルを奨励します。なぜなら、play(x::MyType)はその特定の型に対してのみplayメソッドを定義し、他の型はそれぞれ独自の実装を持つことができるからです。

同様に、非エクスポート関数は通常内部的なものであり、変更される可能性があります。ドキュメントに別途記載がない限りです。名前には、何かが「内部的」または実装の詳細であることをさらに示唆するために、_ プレフィックス(またはサフィックス)が付けられることがありますが、これはルールではありません。

このルールに対する反例には NamedTupleRegexMatchStatStruct が含まれます。

Use naming conventions consistent with Julia base/

  • モジュールと型名は大文字とキャメルケースを使用します: module SparseArraysstruct UnitRange
  • 関数は小文字です(maximumconvert)および、可読性がある場合は複数の単語が一緒に圧縮されています(isequalhaskey)。必要に応じて、単語の区切りとしてアンダースコアを使用します。アンダースコアは、概念の組み合わせを示すためにも使用されます(remotecall_fetchfetch(remotecall(...)) のより効率的な実装として)または修飾子としても使用されます。
  • 関数は、少なくとも1つの引数を変更する場合、!で終わります。
  • 簡潔さは重視されますが、略語は避けてください(indexin のように、indxin のように略すと、特定の単語がどのように略されているかを覚えるのが難しくなります)。

関数名が複数の単語を必要とする場合、それが複数の概念を表している可能性があるかどうかを考慮し、分割した方が良いかもしれません。

Write functions with argument ordering similar to 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. キー。連想コレクションの場合、これはキーと値のペアのキーです。他のインデックス付きコレクションの場合、これはインデックスです。
  7. 。連想コレクションの場合、これはキーと値のペアの値です。fill!(x, v)のような場合、これはvです。
  8. すべてのその他。他のすべての引数。
  9. Varargs. これは、関数呼び出しの最後に無限にリストできる引数を指します。例えば、Matrix{T}(undef, dims)では、次元をTupleのように指定できます。例えば、Matrix{T}(undef, (1,2))や、Varargのように、Matrix{T}(undef, 1, 2)として指定することもできます。
  10. キーワード引数。Juliaでは、キーワード引数は関数定義の最後に来る必要があります。ここでは、完全性のためにリストされています。

ほとんどの関数は、上記に挙げたすべての種類の引数を受け取るわけではありません。数字は、関数に適用可能な引数に対して使用すべき優先順位を示すだけです。

もちろん、いくつかの例外があります。例えば、convertでは、タイプは常に最初に来るべきです。setindex!では、値がインデックスの前に来るため、インデックスを可変引数として提供できます。

APIを設計する際には、この一般的な順序にできるだけ従うことで、関数のユーザーにより一貫した体験を提供できる可能性が高くなります。

Don't overuse try-catch

エラーを捕まえることに頼るよりも、エラーを避ける方が良い。

Don't parenthesize conditions

ジュリアでは、ifwhileの条件の周りに括弧を必要としません。書き方:

if a == b

その代わりに:

if (a == b)

Don't overuse ...

スプライシング関数の引数は中毒性があります。[a..., b...]の代わりに、単に[a; b]を使用してください。これはすでに配列を連結します。collect(a)[a...]よりも優れていますが、aはすでに反復可能であるため、しばしばそのままにしておく方がさらに良いです。配列に変換しないでください。

Ensure constructors return an instance of their own type

メソッド T(x) が型 T に対して呼び出されるとき、一般的には型 T の値を返すことが期待されます。予期しない型を返す constructor を定義すると、混乱を招き、予測不可能な動作を引き起こす可能性があります:

julia> struct Foo{T}
           x::T
       end

julia> Base.Float64(foo::Foo) = Foo(Float64(foo.x))  # Do not define methods like this

julia> Float64(Foo(3))  # Should return `Float64`
Foo{Float64}(3.0)

julia> Foo{Int}(x) = Foo{Float64}(x)  # Do not define methods like this

julia> Foo{Int}(3)  # Should return `Foo{Int}`
Foo{Float64}(3.0)

コードの明瞭さを維持し、型の一貫性を確保するために、常にコンストラクタを設計して、構築すべき型のインスタンスを返すようにしてください。

Don't use unnecessary static parameters

関数シグネチャ:

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

書くべきは:

foo(x::Real) = ...

代わりに、特に T が関数本体で使用されていない場合は。たとえ T が使用されていても、便利であれば typeof(x) に置き換えることができます。パフォーマンスの違いはありません。これは静的パラメータに対する一般的な注意喚起ではなく、必要ない場合の使用に対するものです。

コンテナ型、特に関数呼び出しで型パラメータが必要な場合があることにも注意してください。詳細については、FAQ Avoid fields with abstract containers を参照してください。

Avoid confusion about whether something is an instance or a type

以下のような定義のセットは混乱を招きます:

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

概念が MyType として書かれるべきか、MyType() として書かれるべきかを決定し、それに従ってください。

デフォルトではインスタンスを使用することが好ましいスタイルであり、問題を解決するために必要になった場合にのみ Type{MyType} に関わるメソッドを追加します。

タイプが実質的に列挙型である場合、それは単一の(理想的には不変の構造体または原始型)型として定義されるべきであり、列挙値はそのインスタンスであるべきです。コンストラクタや変換は、値が有効かどうかをチェックできます。この設計は、列挙型を抽象型として定義し、「値」をサブタイプとするよりも好まれます。

Don't overuse macros

マクロが実際には関数であるべき時に注意してください。

evalをマクロ内で呼び出すことは、特に危険な警告サインです。これは、そのマクロがトップレベルで呼び出されたときにのみ機能することを意味します。このようなマクロが代わりに関数として書かれた場合、必要な実行時値に自然にアクセスできるようになります。

Don't expose unsafe operations at the interface level

ネイティブポインタを使用する型がある場合:

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

わかりました。定義のような文は書きません。

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

問題は、このタイプのユーザーが x[i] と書くことができるが、その操作が安全でないことに気づかず、メモリバグにさらされる可能性があることです。

そのような関数は、操作が安全であることを確認するか、呼び出し元に警告するために名前のどこかに unsafe を含めるべきです。

Don't overload methods of base container types

以下のような定義を書くことが可能です:

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

これは特定の新しい要素タイプを持つベクトルのカスタム表示を提供します。魅力的ではありますが、避けるべきです。問題は、ユーザーが Vector() のようなよく知られたタイプが特定の方法で動作することを期待するため、その動作を過度にカスタマイズすると、作業が難しくなる可能性があることです。

Avoid type piracy

「型の略奪」とは、あなたが定義していない型に対して、Baseや他のパッケージのメソッドを拡張または再定義する行為を指します。極端な場合、あなたのメソッドの拡張や再定義が無効な入力をccallに渡す原因となると、Juliaがクラッシュすることがあります。型の略奪はコードの推論を複雑にし、予測や診断が難しい互換性の問題を引き起こす可能性があります。

モジュール内のシンボルに対して乗算を定義したいと仮定します:

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

問題は、現在 Base.* を使用する他のモジュールもこの定義を見ることになることです。Symbol は Base で定義されており、他のモジュールでも使用されているため、無関係なコードの動作が予期せず変更される可能性があります。ここには、異なる関数名を使用することや、定義した別の型で Symbol をラップすることなど、いくつかの代替案があります。

時には、結合されたパッケージが型の海賊行為に関与し、機能を定義から分離することがあります。特に、パッケージが共同著者によって設計され、定義が再利用可能な場合です。例えば、あるパッケージは色を扱うために便利な型を提供するかもしれません。別のパッケージは、その型に対して色空間間の変換を可能にするメソッドを定義することができます。別の例としては、あるパッケージがいくつかのCコードの薄いラッパーとして機能し、別のパッケージがそれを海賊行為して高レベルのJuliaフレンドリーなAPIを実装することが考えられます。

Be careful with type equality

一般的に、型をテストするためには isa<: を使用した方が良いです。== を使用するのは、既知の具体的な型(例えば T == Float64)と比較する場合や、本当に、本当に 自分が何をしているのかを理解している場合にのみ、正確な等価性をチェックすることが意味を持ちます。

Don't write a trivial anonymous function x->f(x) for a named function f

高階関数はしばしば匿名関数と共に呼び出されるため、これが望ましい、あるいは必要であると結論づけるのは簡単です。しかし、任意の関数は匿名関数に「ラップ」されることなく直接渡すことができます。map(x->f(x), a)と書く代わりに、map(f, a)と書いてください。

Avoid using floats for numeric literals in generic code when possible

数値を扱う汎用コードを書く場合、さまざまな数値型の引数で実行されることが期待される場合は、引数に対する昇格の影響をできるだけ少なくする数値型のリテラルを使用してみてください。

例えば、

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

while

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

ご覧のとおり、Int リテラルを使用した2番目のバージョンでは、入力引数の型が保持されましたが、最初のバージョンではそうではありませんでした。これは、例えば 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} を使用して、コードの使用を容易にしてください。