Conversion and Promotion

ジュリアには、数学演算子の引数を共通の型に昇格させるシステムがあり、これは他のさまざまなセクションで言及されています。これには、Integers and Floating-Point NumbersMathematical Operations and Elementary FunctionsTypes、およびMethodsが含まれます。このセクションでは、この昇格システムがどのように機能するか、また新しい型に拡張し、組み込みの数学演算子以外の関数に適用する方法について説明します。伝統的に、プログラミング言語は算術引数の昇格に関して二つのキャンプに分かれます。

  • 組み込みの算術型と演算子の自動昇格。 ほとんどの言語では、+-*/などの中置構文を持つ算術演算子のオペランドとして使用される組み込みの数値型は、期待される結果を生成するために共通の型に自動的に昇格されます。C、Java、Perl、Pythonなどは、その一例であり、1 + 1.5の合計を浮動小数点値2.5として正しく計算します。これは、+のオペランドの一つが整数であるにもかかわらずです。これらのシステムは便利であり、プログラマーにとってほとんど見えないように慎重に設計されています。ほとんどの人は、このような式を書くときにこの昇格が行われることを意識的に考えることはありませんが、コンパイラやインタプリタは、整数と浮動小数点値はそのままでは加算できないため、加算の前に変換を行う必要があります。このような自動変換のための複雑なルールは、したがって、そのような言語の仕様や実装の一部として避けられないものです。
  • 自動昇格なし。 このキャンプにはAdaとMLが含まれており、非常に「厳格な」静的型付け言語です。これらの言語では、すべての変換はプログラマーによって明示的に指定されなければなりません。したがって、例の式 1 + 1.5 は、AdaとMLの両方でコンパイルエラーになります。代わりに、整数 1 を浮動小数点値に明示的に変換してから加算を行うために real(1) + 1.5 と書かなければなりません。しかし、どこでも明示的な変換を行うのは非常に不便であるため、Ada でさえ自動変換の程度があります:整数リテラルは自動的に期待される整数型に昇格され、浮動小数点リテラルも同様に適切な浮動小数点型に昇格されます。

ある意味で、Juliaは「自動昇格なし」のカテゴリーに分類されます:数学的演算子は特別な構文を持つ関数に過ぎず、関数の引数は自動的に変換されることはありません。しかし、さまざまな混合引数型に対して数学的操作を適用することは、ポリモーフィックな複数ディスパッチの極端なケースであることが観察されるかもしれません。これは、Juliaのディスパッチおよび型システムが特に適しているものです。「自動的」な数学的オペランドの昇格は、特別な適用として現れます:Juliaは、オペランド型の特定の組み合わせに対する実装が存在しない場合に呼び出される数学的演算子のための事前定義されたキャッチオールディスパッチルールを備えています。これらのキャッチオールルールは、最初にすべてのオペランドをユーザー定義の昇格ルールを使用して共通の型に昇格させ、その後、結果として得られた同じ型の値に対して問題の演算子の専門的な実装を呼び出します。ユーザー定義の型は、他の型への変換のためのメソッドを定義し、他の型と混合されたときに昇格すべき型を定義するいくつかの昇格ルールを提供することで、この昇格システムに簡単に参加できます。

Conversion

特定の型 T の値を取得する標準的な方法は、その型のコンストラクタ T(x) を呼び出すことです。しかし、プログラマーが明示的に要求しなくても、ある型から別の型に値を変換することが便利な場合があります。1つの例は、値を配列に割り当てることです。もし AVector{Float64} であれば、式 A[1] = 2 は、2Int から Float64 に自動的に変換し、その結果を配列に格納することで機能するべきです。これは convert 関数を介して行われます。

convert 関数は一般的に二つの引数を取ります:最初は型オブジェクトで、二つ目はその型に変換する値です。返される値は、指定された型のインスタンスに変換された値です。この関数を理解する最も簡単な方法は、実際に動作を見てみることです:

julia> x = 12
12

julia> typeof(x)
Int64

julia> xu = convert(UInt8, x)
0x0c

julia> typeof(xu)
UInt8

julia> xf = convert(AbstractFloat, x)
12.0

julia> typeof(xf)
Float64

julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
 1  2  3
 4  5  6

julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 4.0  5.0  6.0

変換は常に可能ではなく、その場合は MethodError がスローされ、convert が要求された変換を実行する方法を知らないことを示します:

julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]

いくつかの言語では、文字列を数値として解析したり、数値を文字列としてフォーマットしたりすることを変換と見なします(多くの動的言語では、自動的に変換を行うことさえあります)。しかし、Juliaではそうではありません。いくつかの文字列は数値として解析できますが、ほとんどの文字列は数値の有効な表現ではなく、その中で有効なものは非常に限られています。したがって、Juliaでは、この操作を実行するために専用の parse 関数を使用する必要があり、より明示的になります。

When is convert called?

次の言語構造は convert を呼び出します:

  • 配列に代入すると、配列の要素型に変換されます。
  • オブジェクトのフィールドに代入すると、そのフィールドの宣言された型に変換されます。
  • オブジェクトを new で構築すると、そのオブジェクトの宣言されたフィールドタイプに変換されます。
  • 変数に宣言された型で代入すること(例:local x::T)は、その型に変換されます。
  • 宣言された戻り値の型を持つ関数は、その戻り値をその型に変換します。
  • ccall に値を渡すと、対応する引数タイプに変換されます。

Conversion vs. Construction

convert(T, x) の動作は T(x) とほぼ同じであることに注意してください。実際、通常はそうです。しかし、重要な意味の違いがあります。convert は暗黙的に呼び出すことができるため、そのメソッドは「安全」または「驚くべきでない」と見なされるケースに制限されています。convert は、同じ基本的な種類のものを表す型間でのみ変換を行います(例:異なる数値の表現や異なる文字列エンコーディング)。また、通常は損失がありません。値を別の型に変換して再び戻すと、正確に同じ値が得られるはずです。

コンストラクタが convert と異なる一般的なケースは4つあります:

Constructors for types unrelated to their arguments

一部のコンストラクタは「変換」の概念を実装していません。たとえば、Timer(2)は2秒のタイマーを作成しますが、これは整数からタイマーへの「変換」ではありません。

Mutable collections

convert(T, x)は、xがすでに型Tである場合、元のxを返すことが期待されます。対照的に、Tが可変コレクション型である場合、T(x)は常に新しいコレクションを作成する必要があります(xから要素をコピーします)。

Wrapper types

いくつかのタイプは他の値を「ラップ」するため、コンストラクタは引数を新しいオブジェクトの中にラップすることがあります。たとえば、Some(x)は、値が存在することを示すためにxをラップします(結果がSomeまたはnothingである可能性があるコンテキストで)。しかし、x自体がオブジェクトSome(y)である場合、結果はSome(Some(y))となり、2つのラッピングレベルになります。一方、convert(Some, x)は、xがすでにSomeであるため、単にxを返します。

Constructors that don't return instances of their own type

非常に稀なケースでは、コンストラクタ T(x)T 型ではないオブジェクトを返すことが意味を持つ場合があります。これは、ラッパー型が自分自身の逆である場合(例:Flip(Flip(x)) === x)や、ライブラリが再構成されたときに後方互換性のために古い呼び出し構文をサポートするために発生する可能性があります。しかし、convert(T, x) は常に T 型の値を返すべきです。

Defining New Conversions

新しい型を定義する際には、最初にその型を作成するすべての方法をコンストラクタとして定義する必要があります。暗黙の変換が有用であることが明らかになり、いくつかのコンストラクタが上記の「安全性」基準を満たしている場合は、convert メソッドを追加できます。これらのメソッドは通常非常にシンプルで、適切なコンストラクタを呼び出すだけで済みます。このような定義は次のようになります:

import Base: convert
convert(::Type{MyType}, x) = MyType(x)

このメソッドの最初の引数の型は Type{MyType} であり、その唯一のインスタンスは MyType です。したがって、このメソッドは最初の引数が型値 MyType のときのみ呼び出されます。最初の引数に使用される構文に注意してください:引数名は :: シンボルの前に省略され、型のみが指定されています。これは、型が指定されているが、値を名前で参照する必要がない関数引数のためのJuliaの構文です。

すべての抽象型のインスタンスは、デフォルトで「十分に類似している」と見なされ、Julia Base では普遍的な convert 定義が提供されています。たとえば、この定義は、1 引数のコンストラクタを呼び出すことで、任意の Number 型を他の任意の型に convert することが有効であると述べています。

convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T

これは、新しい Number 型がコンストラクタを定義するだけで済むことを意味します。この定義が convert を処理します。また、引数がすでに要求された型である場合を処理するために、恒等変換も提供されます。

convert(::Type{T}, x::T) where {T<:Number} = x

AbstractStringAbstractArray、および AbstractDict に対して同様の定義が存在します。

Promotion

Promotionは、異なる型の値を単一の共通型に変換することを指します。厳密には必要ではありませんが、一般的には、値が変換される共通型は元のすべての値を忠実に表現できることが暗黙の前提とされています。この意味で、「promotion」という用語は適切です。なぜなら、値が「より大きな」型、すなわちすべての入力値を単一の共通型で表現できる型に変換されるからです。しかし、これはオブジェクト指向(構造的)スーパーティピングや、Juliaの抽象スーパタイプの概念と混同しないことが重要です。プロモーションは型階層とは無関係であり、代替表現間の変換に関係しています。たとえば、すべての Int32 値は Float64 値としても表現できますが、Int32Float64 のサブタイプではありません。

Juliaでは、promote関数を使用して、共通の「大きな」型への昇格が行われます。この関数は任意の数の引数を受け取り、共通の型に変換された同じ数の値のタプルを返すか、昇格が不可能な場合は例外をスローします。昇格の最も一般的な使用ケースは、数値引数を共通の型に変換することです:

julia> promote(1, 2.5)
(1.0, 2.5)

julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)

julia> promote(2, 3//4)
(2//1, 3//4)

julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)

julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)

julia> promote(1 + 2im, 3//4)
(1//1 + 2//1*im, 3//4 + 0//1*im)

浮動小数点値は、浮動小数点引数型の中で最も大きなものに昇格されます。整数値は、整数引数型の中で最も大きなものに昇格されます。型のサイズが同じで符号が異なる場合は、符号なし型が選択されます。整数と浮動小数点値の混合は、すべての値を保持できる十分な大きさの浮動小数点型に昇格されます。整数と有理数の混合は、有理数に昇格されます。有理数と浮動小数点数の混合は、浮動小数点数に昇格されます。複素数と実数の混合は、適切な種類の複素数に昇格されます。

それがプロモーションを使用するためのすべてです。残りは巧妙な適用の問題であり、最も典型的な「巧妙な」適用は、算術演算子 +-* および / のような数値演算のためのキャッチオールメソッドの定義です。以下は、promotion.jl に示されているいくつかのキャッチオールメソッドの定義です:

+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)

これらのメソッド定義は、数値のペアを加算、減算、乗算、除算するためのより具体的なルールがない場合、値を共通の型に昇格させてから再試行することを示しています。それだけです:算術演算のために共通の数値型への昇格を心配する必要があるのは他にはありません – それは自動的に行われます。promotion.jlの中には、他の多くの算術および数学関数のための包括的な昇格メソッドの定義がありますが、それを超えて、Julia Baseでpromoteを呼び出す必要はほとんどありません。promoteの最も一般的な使用法は、便利のために提供される外部コンストラクターメソッドであり、混合型のコンストラクタ呼び出しを、フィールドが適切な共通型に昇格された内部型に委任することを可能にします。例えば、rational.jlは、次の外部コンストラクターメソッドを提供します:

Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)

これにより、以下のような呼び出しが機能します:

julia> x = Rational(Int8(15),Int32(-5))
-3//1

julia> typeof(x)
Rational{Int32}

ほとんどのユーザー定義型において、プログラマーに対してコンストラクタ関数に期待される型を明示的に指定させることがより良いプラクティスですが、特に数値の問題においては、自動的に昇格を行うことが便利な場合があります。

Defining Promotion Rules

原則として、promote 関数のメソッドを直接定義することは可能ですが、これはすべての引数型の可能な順列に対して多くの冗長な定義を必要とします。代わりに、promote の動作は、promote_rule と呼ばれる補助関数に基づいて定義されており、ここにメソッドを提供することができます。promote_rule 関数は、型オブジェクトのペアを受け取り、引数型のインスタンスが返された型に昇格されるような別の型オブジェクトを返します。したがって、ルールを定義することによって:

import Base: promote_rule
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64

1つは、64ビットと32ビットの浮動小数点値が一緒に昇格されるとき、それらは64ビットの浮動小数点に昇格されるべきであると宣言します。昇格の型は引数の型の1つである必要はありません。たとえば、次の昇格ルールはどちらもJulia Baseで発生します:

promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt

後者の場合、結果の型は BigInt です。これは BigInt が任意精度整数演算のために整数を保持するのに十分な唯一の型であるためです。また、promote_rule(::Type{A}, ::Type{B})promote_rule(::Type{B}, ::Type{A}) の両方を定義する必要はないことにも注意してください。対称性は、promote_rule が昇格プロセスで使用される方法によって暗示されています。

promote_rule 関数は、promote_type という第二の関数を定義するためのビルディングブロックとして使用されます。この関数は、任意の数の型オブジェクトを受け取り、それらの値が promote の引数として昇格されるべき共通の型を返します。したがって、実際の値がない場合に、特定の型の値のコレクションがどの型に昇格するかを知りたい場合は、promote_type を使用できます。

julia> promote_type(Int8, Int64)
Int64

promote_typeを直接オーバーロードするのではなく、promote_ruleをオーバーロードすることに注意してください。promote_typepromote_ruleを使用し、対称性を追加します。直接オーバーロードすると、曖昧さのエラーが発生する可能性があります。私たちは、物事がどのように昇格されるべきかを定義するためにpromote_ruleをオーバーロードし、そのクエリにはpromote_typeを使用します。

内部では、promote_typepromote内で使用され、引数の値が昇格のためにどの型に変換されるべきかを決定します。興味のある読者は、promotion.jlのコードを読むことができ、約35行で完全な昇格メカニズムを定義しています。

Case Study: Rational Promotions

最後に、私たちはジュリアの有理数型に関する進行中のケーススタディを終えます。この型は、以下の昇格ルールを用いて比較的高度な昇格メカニズムを利用しています。

import Base: promote_rule
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)

最初のルールは、任意の他の整数型で有理数を昇格させると、その分子/分母型が他の整数型との昇格の結果となる有理型に昇格することを示しています。第二のルールは、異なる2つの有理数型に同じ論理を適用し、それぞれの分子/分母型の昇格の結果として有理数を生成します。第三の最終ルールは、有理数を浮動小数点数で昇格させると、分子/分母型を浮動小数点数で昇格させたのと同じ型になることを示しています。

この少数の昇格ルールは、型のコンストラクタと数値のデフォルト convert メソッドとともに、有理数がJuliaの他のすべての数値型(整数、浮動小数点数、複素数)と完全に自然に相互運用できるようにするのに十分です。適切な変換メソッドと昇格ルールを同様に提供することで、ユーザー定義の数値型もJuliaの定義済み数値と同様に自然に相互運用できるようになります。