俺のための Julia 入門(4)型宣言

Julia
俺のためのJulia入門
Author

司馬 博文

Published

9/09/2020

概要
Julia は階層関係を明示的に宣言する必要のある名前的型付け言語であり,既存の型から自由な構成が可能なパラメトリック型付け言語である.

1 型の構成

1.1 抽象型

Anyの子の定義:supertype を指定しない場合は Any::DataType が supertype となる.

abstract type Name end

typeof(Name)
DataType

子の定義

abstract type SubtypeName <: Name end
supertype(Name)

subtypes(Name)

fieldnames(Int)

methodswith(Name)

@show Name
Name = Name
Name

<:, >: は arity 2 の論理演算子にもなる.

Int >: Int
true

関数宣言の際の型注釈にて,冠頭演算子としても使う.型変数に対するslicingである.従って,木構造の上でのslicingとなる(第 1.5.2 節).

1.2 複合型 Composite Types

複数の名前付き field をまとめて扱うことができる.

Julia で複合型を立てる時は,まるで構造付きの空間を作るようである.Euclid空間のように.

従って,データと method を統合する(=関数が複合型に所属する)OOP とは少し違い,複合型と,その上の関数を別々に作る.従って,Julia の関数は常 にfirst-class object として扱われる.1

imutable な複合型の変数はstruct-endブロックで宣言する.

struct MyType
    x::Int
    y::Int
end

fieldnames(MyType)
(:x, :y)

複合型を宣言すると,暗黙のうちに constructor が生成される.これも OOP と同じ.

z = MyType(1, 2)
println(z.x)
1

mutable な複合型はmutable struct-endブロックで宣言する.

mutable struct MutableType
    x::Int
    y::Int
end

m = MutableType(3, 4)
m.x = 10  # フィールドxの値を変更可能
10
MyType(x) = MyType(x, 0)
MyType

1.3 Union型:直和

直和型を表す,代数的データ型の簡単な場合.他の言語では Nullable, Option 型と呼ばれる.

Union{Int,String}というように,タグUnionをつけて表す.

function process(x::Union{Int, String})
    if x isa Int
        println("The integer is $x")
    elseif x isa String
        println("The string is $x")
    end
end

process(3)
The integer is 3

Union{Int, Nothing}とすると,部分関数の値域として使える.

1.4 Parametric型

他の言語では generics や template と呼ばれるものである.2

そもそもタグ付という表示方法自体が,タグを関数名と見れば,点を表しているように見える.その調子で,タグを固定し,型変数を導入すればいい.

Parametric 型は,{}オブジェクトで定義する.試しに,Pointというパラメトリック型を定義する:

struct Point{T, U}
    x::T
    y::U
end

インスタンス化する際は,型変数 T, U にも代入をする.

p1 = Point{Int, Float64}(3, 4.5)  # 明示的に型を指定
p2 = Point(3, 4.5)  # 型推論により自動的に型が決まる
p2
Point{Int64, Float64}(3, 4.5)

パラメトリック型のデータに対して関数を定義する際は,型変数を使用した後に,whereを使って型変数を指定する.3

function distance(p::Point{T, U}) where {T, U}
    sqrt(p.x^2 + p.y^2)
end

distance(p2)
5.408326913195984

ただし,この時に,変数Tに関する条件節でif文を分けるのはよくない.

Julia の多重ディスパッチの精神に従って,関数を分けて定義するのが良い.Julia は自動で引数の型に最も適したメソッドを呼ぶ.

即ち,型は parametric や直積にしても,関数は直積にはしない

struct Rectangle{T}
    width::T
    height::T
end

# 型推論によるインスタンス生成
r1 = Rectangle(3.0, 4.0)

# 型指定によるインスタンス生成
r2 = Rectangle{Int}(3, 4)
Rectangle{Int64}(3, 4)
# 多重ディスパッチの実践

function area(r::Rectangle{T}) where T
    r.width * r.height
end

println(area(r1))  # 出力: 12.0
println(area(r2))  # 出力: 12
12.0
12

Point{T}型に対して,型変数Tに代入を行って得る型Point{Int}は concrete 型になり,Point{T}という抽象型を親型に持つ.

従って,多重ディスパッチにおいてはPoint{Int}Point{T}にも match する.4

1.5 型階層

1.5.1 パラメトリック型の位置

型変数を{}演算子により持たせた Parametric 型は,持たせていない型の下流にある.

Point{Int} <: Point
true

「持たせていない型」はUnionAllという型になる.5

typeof(Point)
UnionAll

Julia では,型変数の間に包含関係があっても,一度 parametric になると包含関係は消える【不変】.これは主に実行効率性の理由による.

Point{Int} <: Point{Float64}
false

1.5.2 パラメトリック型の sclicing

UnionAll型はスライシングができる.

function distance(p::Point{<:Number})
    sqrt(p.x^2 + p.y^2)
end

function distance(p::Point{T}) where T <: Number
    sqrt(p.x^2 + p.y^2)
end
distance (generic function with 2 methods)

1.5.3 抽象型と具体型

Number は抽象型であり,Point{Number}という表現はできない.

Point{<:Number}には,Point{Int}もマッチする:

p1 = Point(3, 4)  # Point{Int}
distance(p1)  # 呼び出し成功
5.0

1.5.4 SingletonType:たった一つの型を含む自明な Parametric Type

キーワード Type によって,Type{T}の形で定義される.元の型 T のインスタンスとなる.

isa(Float64, Type{Float64})  # Float64 は Type{Float64} 型のインスタンス
isa(Real, Type{Float64})
false

2 組み込み型とその関数

2.1 Collection 型:直積

更なる構造付きのものはDataStructure.jlにある.

各直積型に,tagをつけてその性質を明示する.

Any直下の型 0. (a,b,…)::Tuple{T1,T2,…} 0. immutableである 0. Array型に対するsize関数の返り値はTuple型. 0. 可変長引数もTuple型のobjectとして受けられる. 0. 上記から観察されるように,入れ物であって,代数的構造を持たせることを意図していない.その場合はArrayを使う. 0. named tuple:Typeの直積. 0. 元々NamedTuple.jlという独立したpackageだったが,0.7から統合. 0. 要素に数字以外のaliasでタグ付できる.immutable. 0. (a=1, b=2)::NamedTuple{(:a, :b), Tuple(Int, Int)}などの記法で定義される. 0. つまり,値のTupleと,Symbol型のオブジェクトのTupleとの組で表される. ⁃ この時の射影が,keys関数,values関数として実装されている. 0. keyは.演算子でアクセスできる. • t.a 0. Symbolをindexの代わりに使える. • t[:a] 0. 一時的に使うのが普通で,本格的にはstruct, mutable structとして第一級の居住権を与えるのが良い. 0. List:Array{T,1}としての実装.Vector{T}というエイリアスもある. 0. 追加や削除などの順序的構造が重視されるCollection型. 0. スタック,キュー,両端キューはDataStructure.jlへ. ⁃ push!(List, object) -> List’ ⁃ 要素の末尾追加 ⁃ pushfirst!(List, object) -> List’ ⁃ 要素の先頭追加 ⁃ pop!(List) -> object ⁃ 要素の末尾摘出 ⁃ popfirst!(List) ⁃ insert!(List, n, object) -> List’ ⁃ n番目の位置に追加 ⁃ deleteat!(List, n) -> List’ ⁃ n番目の要素を削除. 0. 辞書:Dict{K, V}という直積型 0. constructorは • d = Dict{String, Int}() • d[“a”] = 1 • d = Dict(key => value, key => value, …) 0. constructorの{}内の要素を1つ,または全て省略するとAnyとしたのと等価. ⁃ haskey(Dict, key) -> Bool ⁃ Dict型objectに,所定のkeyが含まれているかを判定する. 0. built-inにIdDict型とWeakKeyDict型がある. 0. 集合:Set{T} 0. constructorは • s = Set([1,2]) 0. 即ち,1次元Arrayから生成される.というより,1次元Arrayにタグをつけたものである. 0. 実装は「keyのみを保持する辞書」というべきもので,辞書と同様,値の重複を無視する. ⁃ push!(set, object) ⁃ 値の追加. ⁃ union(set, List) ⁃ intersect(set, List) ⁃ issubset(List, List) -> ::Bool ⁃ List ⊂ List -> ::Bool ⁃ Set型のinstanceを生成することなく集合演算ができる. 0. 多次元配列:Array{T,n}.Matrix{T}はArray{T,2}のエイリアスである. 0. MATLABを踏襲している.NumPyとは所々違う. 0. NumPyは0からindexingし,row-major orderである.これは,行列のindexingにとって,辞書式順序になる. 0. しかしJuliaは1からindexingし,column-major orderである.後者は行列のindexingに沿って,第一要素のストライドが1になる. 0. 従って,Juliaは同じ行の要素の列挙が得意.線型代数のものの見方である. 0. 内部実装は結局一次元配列(List)であることを意識すると良い.

⁃   [] (constructor)
⁃   [ a b c; d e f; h i j ]
⁃   または改行を入れてもいい.
•   []
•   要素へアクセスする作用素.
⁃   isempty(collection) -> Bool
⁃   empty!(collection) -> collection
⁃   空にする
⁃   length(collection) -> Int
⁃   eltype(collection) -> Type
•   関数名の最後に!
•   引数の一部を変更・破棄する関数
•   !のつかない関数は,引数に対する破壊的変更はないので安心して使用できる,という慣習.
•   例:push!,insert!
•   sortは新たなobjectを返すが,sort!は引数そのものを変更してしまう.
•   for文に使う構文はPythonと同じ使用感.
•   しかし,直積型を意識.
•   for (key, value) in d::Dict
•   実装は,iterable型オブジェクトを介して行われる.つまり,Juliaは次のように変換して処理される.速度の問題?
•   next = iterate(collection)while next !== nothing  (x,state) = next  #suite  next = iterate(collection, state)end
•   Juliaはiterable型は無く,Tuple{Int, Int}である.
•   第一要素は,「次の要素」で,第二要素は「その次の要素」のindex(やそれに値するもの)を表す整数またはnothing.
•   従って,自作のcollection型にもfor文iterationを実現させるためには,iterate関数をディスパッチすれば良い.

3 Array型の関数

作成 0. constructor 0. Array{T}(undef, dims…) 0. 値が初期化されていないことに注意. 0. collect(reshape(1:9, 3, 3)) 0. collectionから,要素を奪って配列に仕立て直す. 0. zeros([T,] dims…) 0. ones([T,] dims…) 0. fill(x, dims…) 0. 行列xI 0. fill!(A,x) 0. 配列AをxIにする. 0. rand([T,] dims…) 0. 一様分布でランダムに初期化した配列 0. 型を指定しないとFloat64で. 0. randn([T,] dims…) 0. 正規分布でランダムに初期化した配列 0. similar(A,[T,dims…]) 0. 配列Aと類似した配列を返す. 0. reshape(A, dims…) -> AbstractArray 0. 切り取る,または形を変える. 0. 並びはcolumn-major orderのままである. 0. AにはUnitRange型も許容されるのがすごい. 0. copy(A) 0. deepcopy(A) 0. Aの要素も再帰的にコピーする.

0.  [A B]
0.  数学的記法の感覚で使える行列の接続.
0.  view(A, n, m)  ->  view(::Array{T,n}, m, i)
0.  n, mはindexingまたはslicing.
0.  Aから部分配列を抜き出す.
0.  返るobjectはAへの参照とindexの情報を持っているので,Aを変更するとその部分配列も変わる.
0.  この実装は,Aが巨大すぎる場合への配慮を感じる.
0.  「ただし,現在の計算機による配列のコピー操作は,一般に非常に高速であるため,巨大な配列を扱うのではない限り,サブ配列を作成するよりも,通常のindexingで新たな配列を作成してしまう方が高速であることも多い.この辺りは,実際に計測してパフォーマンスを確かめてみるのが良いだろう.」

属性 0. eltype(A) 0. length(A) 0. ndims(A) 0. size(A) 0. size(A,n) 0. n番目の次元におけるAのサイズ 0. size(A)で返ってくるtupleの第n要素. 0. strides(A) 0. Aのストライド. 0. 第一要素は必ず1になる. 0. 要素同士が,一番浅い意味での一次元配列上でどれくらい離れているか. 0. stride(A,n) 0. n番目の次元におけるAのストライド

0.  [i, j, k, …]または[n:m, i:j, k:l, …]でindexingできる.

代数的構造 0. * 0. 行列乗算 dot付演算子:broadcasting.broadcast(+, A, B)のエイリアスである.broadcast!(+, A, B)とするとAが変更される. 0. component-wiseの演算. 0. A .+ c 0. A + cIと同じ. 0. column-wiseの演算. 0. A .+ B 0. BがAの繰り返し単位になっている場合のみ. 0. つまり,size(B)とsize(A)を各要素ごとに見比べたとき,次の2条件のいずれかを満たすとき. 0. 値が同じ 0. どちらかの値が1 0. 2つ目の条件として,pr_i(size(B)) | pr_i(size(A))だったらもっと使いやすかったがね. 0. broadcast関数 0. broadcast(f, args…)が定義. 0. f.(args…)とも記述できる. 0. ただし,f=+などの時には使えない.fは関数が想定されている. 0. sigmoid.(A)やexp.(A)などである. 0. argsはArrayに限らず,tupleでも良い.

位相的構造 0. map(f, c::collection…) -> collection 0. collection cにelement-wiseにfを適用させる. 0. broadcastingやdot演算と似ているが,fが匿名関数でない場合はdot演算を使うのが良い? 0. reduce(op, itr; [init]) -> obj 0. Aをitrableと見做して,要素ごとにopに突っ込んでいく. 0. 従って,次元が1段階下がる. 0. filter(f, a::collection) -> a’ 0. aを要素ごとにfに突っ込んで,fがfalseを返すものについては脱落させる.

Footnotes

  1. この言葉は 1960 年代に Christopher Strachey によって “functions as first-class citizens” という文脈で初めて使われた。全ての機能を享受できる citizen 的な? ↩︎

  2. Java の generics は Julia に似て不変であるが,C# の generics は in/out キーワードにより共変性をサポートしている.↩︎

  3. where T <:Any の略記↩︎

  4. しかし,Julia の仕様は,多重ディスパッチにおいては concrete 型に近いものが先に適用されるようになっている.↩︎

  5. 意味論的にはそう,全ての型の和集合で,以降はTに代入するたびに同値類のいずれかを得る.↩︎