俺のための Julia 入門(1)データ型

データ型とその上の原始関数

Julia
俺のためのJulia入門
Author

司馬 博文

Published

9/06/2020

Modified

7/04/2024

概要
Julia は動的型付け言語で,型宣言は optional であるが,豊かな型システムを持つ.
Julia の多重ディスパッチを中心とした関数志向設計
  1. 動的型付けであるが,明示的な型付けを型注釈の記法でコードに追加できる.
  2. 多重ディスパッチによって動的にメソッド呼び出しが行われる.Julia で method というと,この多重ディスパッチの方を意味する.1
  3. 一般的な OOP ではなく,オブジェクトに関数を所属させる意味での method は存在せず,型の継承も存在せず,階層関係があるのみである.
Julia のデータ型
  • 全ての型は supertype をもち,Julia の Type はこの親子関係について木構造をなす.
    • root はAnyである.
    • 多重ディスパッチでは,より葉に近い方から match する.
  • concrete型とabstrat型に大別される.
    • concrete型の中でも基底層であるprimitive型は 8bit 単位で指定する.
    • abstract型は instance 化して object を得ることはできない.
    • concrete型はsubtypeを持てない.

1 Built-in Type@base/boot.jl

数値表現の細かい prefix, suffix が多い.

組み込み型一覧
  • Nothing型 <: Any

    • ただ一つの instance であるnothingを持つ.
    • subtypes を訊くと Type[]と返ってきた.
    • nothing objectはREPLでは何も表示されない.return [nothing]と同じ原理.新たな空の概念ですね,記号論が出来る.
  • isnothing(x) -> ::Bool

  • 整数ℤ

    • Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, Uint64, Int128, Uint128
    • Int, UInt型:システムのデフォルトSys.WORD_SIZEに応じて,上記のいずれかのエイリアスとして設定されている.僕のMacは64bitなので64.
    • 符号なし整数型はprefix”0x”を付けて十六進法で表す.
    • ÷/2
      • 整数除算
      • %の共役というか.
  • 真理値TV={true, false}

    • Bool
    • !
      • 論理否定
    • ~
      • bit not
    • &
      • bit and
      • bit or
    • ⊻ (+ Tab)
      • bit xor
      • 右論理シフト
      • 右算術シフト
    • <<
      • 左論理/算術シフト *論理演算はassembly言語の包みと考えるのが良い. 論理演算:word(charやintなど)では届かない,個々のbitに対する演算に対するinterfaceを実装するためにISAや言語に追加された命令操作.初期のコンピュータは「語」単位でデータを扱っていたが,そんなの粒度大きくて仕方がない,という歴史的経緯で後から追加された群.C言語では1bitまで小さいfieldを取れる.(こういう”field”の使い方を,”bit field”という.)
    • shift : 語中の全てのbitを左や右にずらして,空いた部分には0を詰める.n bitの左/右シフトは,2^(±n)を乗ずることに等価.だから,実は配列のindexからbyte addressingに換算してどれくらいbaseからとぶかの計算は,左2シフトである.CやJavaの<<に当たる.
      • AND : 以前はmaskと呼んでいた,隠すからである.maskはAND論理演算のことだったのか!高級言語の&
      • OR, NOR(=ORのNOT) : 左から作用すると考えればちょうどNOT◦OR.それぞれ高級言語の:, ~(これは本来ORの実装)にあたる.
        • NOTはNORを使われて,ISAには等価なものはない.
    • ===
      • メモリ上のobjectとしての同一性.
      • こんな意味論数学上にはない.
  • 小数

    • Float16, Float32, Float64
    • 通常は64で解釈され,32はf0やf-4のsuffixを付けて表す.
    • それぞれのサイズに対して,値Inf16, Inf32, Infが無限大を表す値として用意されている.
    • 0/0などの「未定義」値として,NaN16, NaN32, NaNが用意されている.
    • /2
      • x/yの共役演算
  • 複素数ℂ

    • 虚数単位はim
    • real(x)
    • imag(x)
    • conj(x)
    • abs(x)
  • 任意精度演算

    • 任意精度整数:BigInt,任意精度小数:BigFloat
  • 定数

    • 宣言constをつけてから定義する.
    • 組込定数
      • pi
      • VERSION

2 Collection 型

全てのobjectに,indexing, slicingの操作が施せる.

indexing - [n] - 最初から数えて第n byteのobjectを返す. - 日本語は3byte表現されていることに注意. - [end] - 最後の要素のindexing.長さがわからないときに使う. - [end-n] - 末尾からのindexing slicing * n:m * UnitRange{Int}型object. * for文にも使える. - [n:m] - n番目からm番目までの閉区間を切り取る. - n=mの時,indexingとは違ってString型を返す.

  • 文字列
    • Char, String
      • UTF-8符号化を用いているので,Unicode文字列がサポートされている.
        • UTF-8は可変長の符号化なので,indexingについては注意しなきゃいけない.
    • “ … “ #constructor
      • String型を作る.
    • ‘ … ‘ #constructor
      • Char型を作る.
        • 連結演算子
    • string(x[,y,…])
      • 連結コンストラクタ.
    • $
      • 文字列補間(interpolation)演算子
      • $(obj)で,objに格納されたString型オブジェクトに置換される.
      • $(1+2)は評価されてから文字列に変換される.
    • Vector{Char::DataType}(s::String) -> Array
      • Charと指定したデータの配列にデータ型を変換する.するとindexingが直感的にやりやすくなる.
      • これはconstructorに(s)で作用させているのか.
    • length(s)
    • repeat(s,n::int)
    • replace(str, s => t)
      • 論理でいえばstr[s::=t]
    • split(str, delimitar) -> Array{SubString{String}, n}
      • delimitarの部分で分割し,n要素のString-配列を返す.
    • startswith(str1, str2) -> TV::Bool
      • str1がstr2の文字列を始切片として持つ場合trueを返す.
    • endswith(str1, str2)
    • join([str1, str2]::Array{String}, delimitar} -> String
      • delimitarで区切りながら連結
    • findfirst(str1, str2) -> slicing object::UnitRange{Int64}
      • str1の先頭から,str2にマッチする部分を検索し,見つかったらその最初の要素の範囲を閉区間で返す.
  • 正規表現object::Regex
    • 文字列型データの前にrをつけることで表す.
    • PCRE (Perl compatible regular expressions)ライブラリでサポートされている.
    • match(regex::Regex, string) -> RegexMatch(“substring”::Substring{String})::RegexMatch
      • matchingがなかった場合はnothing::Nothingが返ってくる.
  • RegexMatch
    1. m.match::Substring{String}:matchした文字列が格納されている.
    2. m.offset::Int:マッチした位置を表すindexが格納されている.
  • 配列

3 型定義

::演算子で型宣言をする.これを省略するとx::Anyの略記とみなされる.

型定義

  1. Anyの子の定義

    abstract type Name end
  2. 親を指定した定義

    abstract type Name <: Supertype end
  • supertype(typename::DataType) -> typename::DataType
  • subtypes(typename::DataType) -> typename::Array{Any, 1}
    • subtypeがない場合はType[]という空のリストが返ってくる.
  • 型の階層
    • <:演算子/>:演算子
      • Anyの子ではない型の型宣言の時に用いる.
      • arity 2の論理演算子としても使う.木構造を生成する始切片と∈の関係に対する⊂と同じ構文論を持つ.特にInt <: Int.
      • また関数宣言の際の型注釈にて,冠頭演算子としても使う.型変数に対するslicingである.従って,木構造の上でのslicingとなる.
    • supertypeを指定しない場合はAny::DataTypeがsupertypeとなる.
    • primitive型は

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

Juliaで複合型を立てる時は,まるで構造付きの空間を作るようである.Euclid空間のように. 従って,データとmethodを統合する(=関数が複合型に所属する)OOPとは少し違い,複合型と,その上の関数を別々に作る. 従って,Juliaの関数は常にfirst-class objectとして扱われる. この言葉は1960年代にChristopher Stracheyによって「functions as first-class citizens」という文脈で初めて使われた。全ての機能を享受できるcitizen的な?

  1. 複合型の変数はstruct - endブロックで宣言する. struct Type x::Int y::Int end
  2. function(p::Type)として関数を作る.すると,その中でx,yというfieldが使える.
  • fieldの値には,OOPのようにdot演算子でアクセスすることができる.
  • 複合型を宣言すると,暗黙のうちにconstructorが生成される.これもOOPと同じ.
    • constructorは次のような代入ができる.固定というか.
      • Type(x) = Type(x,a)
  • struct宣言による複合型のfieldはimmutableである.
    • structのfiledとしてのcollection型のデータは変更できないが,その要素は変更できる.
    • mutable structで宣言するとfieldはmutableになる.

Union型:直和 直和型を表す,代数的データ型の簡単な場合.

  • Union{Int,String}というように,タグ”Union”をつけて表す.
    • Union{Int, Nothing}とすると,部分関数の値域として使える.
      • 他の言語ではNullable, Option型と呼ばれる.

Parametric型 他の言語ではgenericsやtemplateと呼ばれる.JavaのgenericsはJuliaに似て不変であるが,C#のgenericsはin/outキーワードにより共変性をサポートしている. そもそもタグ付という表示方法自体が,タグを関数名と見れば,点を表しているように見える.その調子で,タグを固定し,型変数を導入すればいい.

  1. Parametric型は,{}オブジェクトで定義する. struct Point{T,U}
  2. constructorでinstanceを生成する場合は,Point{Int, Float}(3,4)と全てに引数を与えても良いが,型変数への代入は省略してもいい.すると,暗黙に型付けが行われる.
  • Parametric型のデータの上に関数を定義する際,型変数Tを用いたら,where Tを添える必要がある(where T <:Anyの略記). function distance(p::Point{T,U}) where {T,U} sqrt(p.x2+p.y2) end
    • ただし,この時に,変数Tに関する条件節でif文を分けるのはよくない.Juliaの多重ディスパッチの精神に従って,関数を分けて定義するのが良い.
    • 即ち,型はparametricや直積にしても,関数は直積にはしない.
  • Point{T}型に対して,型変数Tに代入を行って得る型Point{Int}はconcrete型になり,Point{T}という抽象型を親型に持つ.
    • 従って,多重ディスパッチにおいてはPoint{Int}はPoint{T}にもmatchする.しかし,Juliaの仕様は,多重ディスパッチにおいてはconcrete型に近いものが先に適用されるようになっている.

階層関係の乗り方について:compatibleに定義されている. * 型変数を{}演算子により持たせた型は,持たせていない型の下流にある. * Point{Int} <: Point -> true * この「持たせていない型」はUnionAllという型になる.意味論的にはそう,全ての型の和集合で,以降はTに代入するたびに同値類のいずれかを得る. * Juliaでは,型変数の間に包含関係があっても,一度parametricになると包含関係は消える【不変】. * Point{Int} <: Point{Float64} -> false * これは主に実行効率性の理由による. * <:冠頭演算子:従って関数宣言の時に注意すべきこと. * 型変数のための型UnionAllにアクセスする演算子.PointはUnionAllだがPoint{<:Any}と考えた方がいい. * function distance(p::Point{Number})はhitしない. * 多分.抽象型Numberのinstanceなんて存在しないので. * function distance(p::Point{<:Number}) * function distance(p::Point{T}) where T <:Number * Point{Int}などの具体的なDataTypeは具体型で子を持たないので,UnionAllを使わなきゃいけない,ということ. * 抽象型のパラメトリック型はやばい.

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

全然何が起こっているかわからない. julia> isa(Float64, Type{Float64}) true julia> isa(Real, Type{Float64}) false

Collection型:直積 更なる構造付きのものはDataStructure.jlにある. 各直積型に,tagをつけてその性質を明示する.

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

Array型の関数

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

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

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

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

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

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

Footnotes

  1. Julia でメソッドというと関数の多重ディスパッチのことであり,特定のクラスへの所属を含意しない.この意味で,Julia はオブジェクト志向というより関数志向・プロセス志向である.Stefan Karpinski (2019) The Unreasonable Effectiveness of Multiple Dispatch も参照.↩︎