変換と昇格

`

変換と昇格

整数と浮動小数点数算術処理と基本的な関数メソッド、その他のさまざまなセクションに記述しているように、Juliaには算術演算子の引数を共通の型に昇格するシステムが備わっています。 このセクションでは、この昇格システムが動作するしくみや、昇格システムを新しい型に拡張して、組込みの算術演算子や関数でも動作させる方法について説明します。 従来、プログラミング言語は算術演算の引数がどのように昇格するかによって、2つの陣営に分類されています。

ほとんどの言語では、組込みの数値型が、+-*/などの中置記法の算術演算の被演算子として使われるときは、自動的に共通の型に昇格してから、計算結果が算出されます。 少し例を挙げると、 C、Java、Perl、Pythonなどはすべて、1 + 1.5の合計を、浮動小数点値の2.5として正しく計算することができますが、+の被演算子の一方は整数です。 こういうシステムは便利であり、通常はプログラマには、ほとんど見えないように慎重に設計されています。 このような式を書くときに昇格が起こっていると意識する人はほとんどいませんが、コンパイラやインタプリタは、足し算を行う前に変換を必ずおこないます。 整数と浮動小数点数はそのままでは足せないからです。このような自動変換の複雑な規則は、必然的にこの陣営の言語では仕様や実装の一部となります。

ある意味、Juliaは「非自動昇格」の陣営に分類されるでしょう。 Juliaでは、算術演算子は特殊な構文を持つ関数に過ぎず、関数の引数は決して自動的に変換されません。 しかし、様々な型の混合した引数に算術演算を適用することは、多相的な多重ディスパッチの極端な事例に過ぎません。 これは型によるディスパッチをおこなうJuliaのシステムに非常に適しています。 算術演算でおこる被演算子の「自動」昇格は、特殊な適用がおこるだけです。 Juliaには、算術演算子を全捕捉するディスパッチ規則が事前に定義されており、被演算子の型の組み合わせに対して特化した実装が存在しないときに呼び出されます。 この全捕捉規則は、まずユーザーの定義可能な昇格規則を利用してすべての被演算子を共通の型に昇格し、こうして同一となった型に特化した演算子の実装を呼び出し、演算結果を算出します。 ユーザーの定義した型もこの昇格システムに簡単に追加できます。 ユーザー定義型に対して、他の型と相互に変換するメソッドを定義し、他の型が混在する場合にどの型に昇格するかを定義する少数の昇格規則を決めてやればいいのです。

`

変換

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

このconvert関数は通常、2つの引数をとります。1番目は型オブジェクトで、2番目はその型に変換する値です。 戻り値は指定した型のインスタンスに変換された値です。 この関数を理解する最も簡単な方法は、実際の動作を見ることです。

julia> x = 12
12

julia> typeof(x)
Int64

julia> convert(UInt8, x)
0x0c

julia> typeof(ans)
UInt8

julia> convert(AbstractFloat, x)
12.0

julia> typeof(ans)
Float64

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

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

変換は常に可能であるとは限りません。 そんな時は、メソッドがないことを示すエラーが投げられ、convert関数が要求された変換の実行方法が分からないことを通知します。

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

言語の中には文字列を解析して数値とみなしたり、書式付きの数値を変換して文字列とみなしたりするものがありますが、 (多くの動的言語ではさらに自動的に変換まで行います)、Juliaはそうではありません。 文字列の中には数値として解析できるものもありますが、ほとんどの文字列は数値の有効な表現ではなく、 非常に限られた一部が当てはまるだけです。 そのため、Juliaでは、こういった操作は、専用の関数parse() を使って明示的に行う必要があります。

[](### When isconvert` called?)

変換が呼ばれるのはいつ?

以下のような言語の構文ではconvertが呼び出されます。

`

変換 vs. 生成

注意点があります。 convert(T, x)の挙動はT(x)とほとんど同じで、実際に大抵の場合同じです。 しかし、セマンティック上の重要な違いがあります。 convertは暗黙裏に呼び出されるので、このメソッドの使用は、「安全」で「驚かない」場合のみに制限されます。 convertは、基本的には同じ種類の表現どうしの間で変換を行います。 (例えば、数字の異なる表現や文字列の異なるエンコーディングなど) また、通常は損失は起こりません。別の型に変換してから元に戻すと元の値ちょうど等しい値に戻ります。

コンストラクタとconvertが異なる4種類の一般の場合があります。

`

引数と無関係な型のコンストラクタ

コンストラクタの中には、「変換」という概念を実装していないものがあります。 例えば、Timer(2)は2秒のタイマーを生成し、実際に整数をタイマーに「変換する」わけではありません。

`

可変コレクション

convert(T, x)xが既に型Tである時、元のxを返しますが、 Tが可変コレクションの場合、Tは常に新しいコレクションを生成します。(xの要素をコピーします)

`

ラッパー型

他の値を「ラップする」型では、コンストラクタは引数を新しいオブジェクトの中にラップします。 引数が既に要求された型であった場合でさえもです。 例えば、Some(x)xをラップし、値が存在することを(結果がSomenothingになるという文脈で)示します。 しかし、x自体がSome(y)出会った場合、結果はSome(Some(y))となり、二重にラップされます。 一方convert(Some, x)は単なるxを返します。xが既にSomeだったからです。

`

自身と同じ型のインスタンスを返さないコンストラクタ

非常に稀に コンストラクタT(x)が型がTではないオブジェクトを正常であっても返す場合があります。 これは、ラッパーの型が自身の逆写像(つまりFlip(Flip(x)) === x)だったり、 ライブラリが再構成された時に、後方互換性のために、古い構文が呼び出しに対応したりする場合に起こります。 しかし、convert(T, x)は常に型Tの値を返します。

`

新しい変換の定義

新しい型を定義する時は、まず、すべての型を生成する方法を、コンストラクタとして定義すべきです。 暗黙の変換が有用だとわかり、コンストラクタが上記の「安全」の条件を満たす時はconvertメソッドを追加しても構いません。 普通は、メソッドはとても単純です。適切なコンストラクタを呼ぶだけでいいからです。 そういった定義は、こんな感じになります。

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

このメソッドの1番目の引数の型は[シングルトン型](@ ref man-singleton-types)のType{MyType}で、この型のインスタンスはMyTypeだけです。 したがって、このメソッドは、最初の引数が型の値を示すMyTypeである場合にのみ呼び出されます。 最初の引数の構文に注目してください。 ::という記号の前にあるはずの引数名は省略され、型だけが指定されています。 これはJuliaの関数の構文で、引数の型は指定するけれども、引数の値は関数本体でまったく使わない時に用います。 この例では、型はシングルトンであるため、引数の名前で参照しなくてもその値がわかります。

ある種の抽象型のインスタンスすべては、デフォルトでは「十分似ている」とみなされて、 JuliaのBaseライブラリの中で普遍的なconvertが定義されています。 例えば、任意のNumber型から他のNumber型への有効なconvertの定義を、 1引数のコンストラクタを呼び出すことによって行っています。

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

これが意味するのは、新しいNumber型にはコンストラクタを定義するだけ良いということです。 convertの定義が扱うのは、コンストラクタだけだからです。 恒等変換も引数がすでに要望される型である時に扱えます。

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

同様の定義がAbstractStringAbstractArrayAbstractDictに存在します。

`

昇格

昇格は、様々な型の値を単一の共通の型に変換することを指します。 共通の型に変換した値は、元の値を全く忠実に表現していることが、必ずしも必要ありませんが、通常は想定されています。 この意味では、「昇格」という用語は適切で、値は「より大きな」型、 つまり、入力した値のすべてを表せる単一の共通の型に変換されます。 しかし重要な点ですが、昇格をオブジェクト指向の(構造的)スーパータイプや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> Rational(Int8(15),Int32(-5))
-3//1

julia> typeof(ans)
Rational{Int32}

ユーザー定義型は、プログラマーがコンストラクター関数に対して想定する型を明示的に指定するのが大抵の場合はよいと思いますが、 時には、特に数値問題の場合は、自動昇格を使うと、便利なものです。

`

昇格規則の定義

原理的にはpromote関数のメソッドを直接定義することができますが、とりうる引数の型の順列すべてに対する数多くの冗長な定義が必要になります。 その代わりに、promote関数の挙動の定義を、promote_ruleという補助的な関数のメソッドを定義して行うことができます。 このpromote_rule関数は、型オブジェクトの組を引数に取って、別の型オブジェクトを返しますが これは引数のインスタンスの型が戻り値の型に昇格することを意味するので、これを使って規則を定義します。

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

64ビット浮動小数点数と32ビット浮動小数点数を一緒に昇格するときは、64ビット浮動小数点数に昇格する必要があります。 しかし昇格後の型は引数の型のどれかである必要はありません。 次の昇格規則は共にJuliaの標準ライブラリにあるものです。

promote_rule(::Type{UInt8}, ::Type{Int8}) = Int
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という第2の関数を定義します。 promote_type関数は、任意の数の型オブジェクトを引数にとり、これらの値の共通の型を返します。 この型がpromote関数が引数を昇格後にとる型となります。 したがって、実際の値がなくても、promote_typeを使えば、型の決まった値の集合が昇格するとどんな型になるかを調べることができます。

julia> promote_type(Int8, Int64)
Int64

内部的には、promote_typepromoteの内部で、引数値を昇格してどんな型に変換するかを決定するために使用されますが、promote_type単体でも有用なことがあります。 興味のある読者は約35行で完全な昇格の仕組みを定義するコードを promotion.jlで読むことができます。

`

事例研究: 有理数

とうとう、ここまで進めてきたJuliaの有理数型の事例研究が完成します。ここでは、以下の昇格規則による昇格メカニズムを比較的洗練された手法で利用しています。

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)

第1の規則は、有理数型と整数型を昇格すると、有理数型に昇格し、その分子/分母の型は元の有理数の分子/分母の型と整数型を昇格した型になることを示しています。 第2の規則は、2つの異なる有理数型を昇格すると、同様の論理を適用して、各有理数型の分子/分母の型を昇格した型を分子/分母の型とするような有理数型に昇格することを示しています。 最後の3つ目の規則は、有理数型と浮動小数点型を昇格すると、浮動小数点型に昇格し、その型は有理数型の分子/分母の型と浮動小数点数型を昇格した結果と同じ型になることを示しています。

この少数の昇格規則と、[前述の変換メソッド]だけで十分、有理数型をとても自然にJuliaの他の数値型、つまり 整数、浮動小数点数、複素数と一緒に使うことができます。 同様に、適切な変換メソッドと昇格規則を定義すれば、どんなユーザー定義の数値型でも自然に、Juliaで事前定義されている数値型と一緒に使うことができます。