Juliaのtype systemの基本についてざっくりと

  • 4
    いいね
  • 0
    コメント

Julia advent calendar 2016 7日目の記事です.Juliaのtype system(型システム)についてざっくりとまとめました.

全てのtypeは樹形図のようにつながっている

樹形図と言えば,根があって,枝分かれがあって,最後に葉にたどり着くような図のことだけど,Juliaのtype systemはまさにこれ.

Subtypeとsupertype

今,ある一つのtype Aがあったとしよう.そこから根の方向にあるtype Btype Asupertypeと呼ぶ.逆にtype Aから葉の方向にあるtype Ctype Asubtype.これを踏まえて,Juliaにおける全てのtypeのsupertype,すなわち根に当たるtypeAnyと命名されている.つまり,全てのtypeAnyのsubtypeと言うことも出来る.

julia> Float64 <: Any
true

julia> Any <: Float64
false

julia> Float64 <: Real
true

julia> Float64 <: String
false

<:Float64 <: Anyの例では"Float64Anyのsubtypeである"と言う意味になる.結果としてtrueが返ってきてるから正しいことがわかる.他の例では,Float64は実数を示すRealのsubtypeであることがわかる.全然関係ないFloat64Stringの結果はもちろんfalse

直近のsubtypeやsupertypeを調べるには,subtypessupertypeという関数を用いれば良い.subtypesにだけ複数形の"s"が付いていることに注意が必要.これには,枝は分かれることはあっても,くっつくことはないという重要な意味が込められており,多重継承ができないことを意味している.

では,実際に2つの関数を使ってみよう.

julia> subtypes(Real)
4-element Array{Any,1}:
 AbstractFloat
 Integer
 Irrational{sym}
 Rational{T<:Integer}

julia> supertype(String)
AbstractString

julia> supertype(Any)
Any

Abstract typeとconcrete type

ここからはもう一つ違った視点でtypeを見てみよう.Juliaにはconcrete typeとabstract typeがある.abstract typeはsubtypeを持つことができるが,concrete typeはsubtypeを持つことができない.つまり,concrete typeは継承することができず,必ず樹形図の葉の位置にある

着目しているtypeがabstractなのかconcreteなのかを調べたい場合は,正攻法かは分からないが,Float64.abstractReal.abstractなどとすれば分かる.

julia> Float64.abstract
false

julia> Real.abstract
true

樹形図を見てみる

例えば次のような関数を作ってみる.

print_tree.jl
function print_tree(typ)
    println(typ)
    print_tree(typ, [])
end

function print_tree(typ, level)
    stypes = subtypes(typ)
    for stype in stypes
        if stype !== stypes[end]
            println(join(level) * "├─── $stype")
            push!(level, "|    ")
        else
            println(join(level) * "└─── $stype")
            push!(level, "     ")
        end
        print_tree(stype, level)
        pop!(level)
    end
end

これを使うと次のようにJuliaの樹形図の構造を見ることができる.

julia> print_tree(Real)
Real
├─── AbstractFloat
|    ├─── BigFloat
|    ├─── Float16
|    ├─── Float32
|    └─── Float64
├─── Integer
|    ├─── BigInt
|    ├─── Bool
|    ├─── Signed
|    |    ├─── Int128
|    |    ├─── Int16
|    |    ├─── Int32
|    |    ├─── Int64
|    |    └─── Int8
|    └─── Unsigned
|         ├─── UInt128
|         ├─── UInt16
|         ├─── UInt32
|         ├─── UInt64
|         └─── UInt8
├─── Irrational{sym}
└─── Rational{T<:Integer}

引数にAnyを渡すとどえらいことになるのでオススメしません.

typeを実際に作ってみる

ここからは,実際にtypeを作ってみるけど,単に作るだけならとっても簡単.

type Person
    name
    age::Int
end

この時点でデフォルトコンストラクタが作られ,コンストラクタを呼ぶ際には引数に変数を定義した順番で与えれば良い.

julia> p = Person("Julia", 23)
Person("Julia",23)

julia> typeof(p)
Person

julia> p.name
"Julia"

julia> p.age
23

なお,ここで作ったtypeはcomposite typeと呼ばれ,先に述べたconcrete typeの一つ.

abstract typeは以下のようにabstractキーワードを使って作れる.

julia> abstract AbstractPerson

julia> AbstractPerson.abstract
true

typeの継承

typeの継承とは,あるtypeのsubtypeを作ること.だから,先にも言ったようにabstract typeからしか継承はできない.継承するには<:を使う.

abstract Animal

type Dog <: Animal
    name::String
    strength::Int
end

type Cat <: Animal
    name::String
    strength::Int
end

これで,DogCatAnimalのsubtypeとなる.

julia> print_tree(Animal)
Animal
├─── Cat
└─── Dog

継承をして何がおいしいの?

継承する利点はいろいろあるけど,例えば簡単な例でいうと,supertypeで定義されたmethodはsubtypeでも使えるというのがある.

function fight(anim1::Animal, anim2::Animal)
    if anim1.strength == anim2.strength
        println("Draw")
    elseif anim1.strength > anim2.strength
        println("$anim1 beats $anim2")
    else
        println("$anim2 beats $anim1")
    end
end

こんな関数を定義すると,AnimalのsubtypeであるDogCat両方を引数として渡すことができる.

julia> dog = Dog("John", 85)
Dog("John",85)

julia> cat = Cat("Julia", 100)
Cat("Julia",100)

julia> fight(dog, cat)
Cat("Julia",100) beats Dog("John",85)

Parametric type

ここまでは,単純なcomposite typeとabstract typeについてやってきたけど,他の便利なtypeとしてparametric typeというものがある.これはc++でいうclass template的なやつ.

type Point{T}
    coordinate::T
end

コンストラクタを呼び出すと,

julia> p1 = Point(1.0)
Point{Float64}(1.0)

julia> p2 = Point([1.0,2.0])
Point{Array{Float64,1}}([1.0,2.0])

というように,Point{}の中にTのtypeが含まれていることがわかる.ちなみにabstract typeでは,

abstract AbstractPoint{T}

と書く.

さらに,Tのtypeを限定させることもできる.例えば,

type AnimalGroup{T <: Animal}
    animals::Vector{T}
end

とすれば,変数animalsのtypeはVector{T}で,そのTAnimalのsubtypeであるという意味になる.実際に使ってみると,

julia> AnimalGroup("Cat")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AnimalGroup{T<:Animal}
This may have arisen from a call to the constructor AnimalGroup{T<:Animal}(...),
since type constructors fall back to convert methods.
 in AnimalGroup{T<:Animal}(::String) at ./sysimg.jl:53

julia> dog = Dog("John", 85)
Dog("John",85)

julia> cat = Cat("Julia", 100)
Cat("Julia",100)

julia> AnimalGroup([dog,cat])
AnimalGroup{Animal}(Animal[Dog("John",85),Cat("Julia",100)])

というように,String型など間違った引数を与えたときはエラーが出る.

Parametric typeを使って関数を定義する際の注意点

先のPointを使って示すと,まず

julia> Float64 <: Real
true

julia> Point{Float64} <: Point{Real}
false

であることに注意が必要.つまり,パラメータTの関係はtypePointに引き継がれないTupleは例外(後述)).これは,abstract typeでも同じ.

julia> AbstractPoint{Float64} <: AbstractPoint{Real}
false

なので,

function hello(p::Point{Real})
    println("hello")
end

という関数を作っても,

julia> p_float = Point(2.0)
Point{Float64}(2.0)

julia> hello(p_float)
ERROR: MethodError: no method matching hello(::Point{Float64})
Closest candidates are:
  hello(::Point{Real}) at REPL[33]:2

julia> p_int = Point(2)
Point{Int64}(2)

julia> hello(p_int)
ERROR: MethodError: no method matching hello(::Point{Int64})
Closest candidates are:
  hello(::Point{Real}) at REPL[33]:2

エラーとなり,うまく限定化できない.正しくは,

function hello{T <: Real}(p::Point{T})
    println("hello")
end

と定義してやる必要があり,この場合は,"引数はPoint{T}で,TRealのsubtype"という意味になる.

ここで議論したことは,自作したPointに限らずVector{...}などを使うときも常に注意が必要.

例外:Tupleはパラメータを引き継ぐ.

julia> Tuple{Float64} <: Tuple{Real}
true

julia> Tuple{Float64, String} <: Tuple{Real, AbstractString}
true

julia> Tuple{Float64, String, Int64} <: Tuple{Real, AbstractString, Float64}
false

このように,複数あった場合は順番に評価され,全てtrueだった場合のみtrueが返ってくる.

この投稿は Julia Advent Calendar 20167日目の記事です。