<< (newer post) (older post) >>     

超入門


様々な情報を入手

いつでもヘルプ

Julia 環境ではいつでも、先頭に ? をつけて ?命令 とするとその命令のマニュアルをその場で読むことが出来る. 例えば、行列の指数関数(後述する)である expm() について、

?expm

としてみよう. 以降、困ったらまずこの機能を使ってみよう.

ドキュメント

jupyter 環境では、上の “Help” 経由で Julia 自体やそのパッケージについてマニュアルを web 経由で参照することが出来る. 気軽に見てみよう.

あと、日本語で書かれていて、かつ、クリエイティブ・コモンズライセンスで公開されている、分かりやすい解説資料 JuliaBook C89版, C90版 がある. 入手して読んでおくと、大変大変ためになる.

動作をみてみる

コマンドや式が実際にどのような動作しているのかを少しだけ詳しく見てみたければ、そのコマンド等の前に @code_lowered をつけて実行してみれば良い. 例えば、

@code_lowered sin(sqrt(3))

などとしてみよう.


変数の扱い, 型

変数は宣言なしでいきなり使えて、型は内容で自動的に決まる. 例えば、スカラーの数値変数ならば

n = 48  # 整数型(Int64 など)になる
x = 1.2  # 浮動小数型(Float64 など)
a = 2//3  # 有理数
c = 1.0 + 2.0im  # 複素数. im は純虚数単位.

という感じだ. 有理数や複素数が最初から使えるので、大変便利だ.

型を明示的に決めたい場合は、数値の型変換関数を使って、

m  = Int8(56)

などとすると、例えばこの変数 $m$ は Int8 型の変数で、中身は 56 ということになる.

ただし、多倍長型(BigInt, BigFloat)に具体的な数値を渡す時は注意した方がいい. 例えば、0.1 を多倍長で扱う時は、関数 big を使って文字列で

b = big"0.1" # 多倍長型(BigFloat).多倍長型は数字を文字列で渡すと、いっぱいまで精度にあわせて変換する.
1.000000000000000000000000000000000000000000000000000000000000000000000000000002e-01

というようにデータを渡す方が良い(big 特有の機能).

というのも、

b = big(0.1)

として渡すと、右辺で 0.1 が Float64 と解釈されて 64bit 分だけ正しい値に変換され、その後あらためて多倍長に変換されるため、

1.000000000000000055511151231257827021181583404541015625000000000000000000000000e-01

というように 0.1 からの誤差が随分と大きくなってしまう.

どんな型があるのか

Julia の基本の型は、次のような感じで tree になっている.よって、動作を色々と自然に拡張できる.

  • 全ての具体型はなんらかの抽象型の「子」である
  • Any 型を除く全ての抽象型は「より抽象的な」抽象型の「子」である.
  • Any 型が tree の頂点にある.

あとは、この基本の型をもとにいろいろな型が作られている. どんな型があるかは、例えば本家ドキュメントの Integers and Floating-Point Numbers を見ると分かりやすい.

型を知るには

typeof とするとその変数の型を知ることが出来る.例えば、上の変数 $c$ に対して typeof(c) とすると Complex{Float64} と得られる.

演算結果の型

演算を行うと、「無理のない方へ」結果の型は変換される(promotion)ので、心配は不要だ. n + x は 49.2 になるし、 x + a は 1.866666… になるし、 n + a は 164//3 になるし、 x + c は 2.0 + 2.0im になる.

少し自分でいろいろ計算してみよう.

他言語に詳しい人は整数同士の割り算に少し注意が要る. 例えば 3 / 21 では無く、1.5 になるのだ. 数値計算としてはこの動作のほうが無理がないが、C や Fortran などの動作と違うので気をつけよう. なお、整数同士の割り算は div(3,2) などとする.


単なる集合(タプル)

変数や数値をカンマで区切って単に複数並べたものをタプルと呼ぶ. 要素の型はバラバラでいい. タプルは 丸括弧 で囲まなくても良いが、囲んだほうが文法上安全なことが多いかな.

その要素には、[要素番号] をつけるとアクセスできる.ちなみに番号は 1 から.

地味に使い勝手が良い存在で、例えば次のようなことができる.

x = 3.0
y = 1.2

x,y = y,x # 値のスワップが簡単に…

複数の値を受け渡しする時は便利な機能なので、覚えておくと良いだろう.

なお、配列(後述)のような豊富な操作手段が用意されているわけではないので、大量だったり複雑だったりする使い方には向かないだろう.


配列、ベクトル、行列

Julia では配列の要素は全て同じ型. そして、Julia では 縦ベクトル := 1次元配列、行列 := 2次元配列という定義なので、線形代数的にはこちらの見方の方が重要かも.

ちなみに横ベクトルは 行列=2次元配列 の一種という扱いなので、少し注意が必要だ.

そして、縦ベクトル = 1次元配列は、[ ] で囲んで要素をカンマで区切れば良い.例えば次のような感じで作れる.

a = [ 1, 5, 12, 4 ]

縦ベクトルだから、見た目もそう書きたいというのであれば、実際に改行して入力しても良い.

a = [ 
    1
    5
   12
    4
 ]

また、セミコロン ; は改行の代わりになるので、これを使っても良い.

a = [ 1; 5; 12; 4 ]

上の3つの入力方法はまったく同じ結果になる.

行列 = 2次元配列は、要素を空白で区切り、改行していけばよい. 例えば

A = [ 
    1 3 8
    2 -5 10
    -3 1 7
 ]

とすると

3×3 Array{Int64,2}:
  1   3   8
  2  -5  10
 -3   1   7

となり、うまく定義できたことが分かる. また、ベクトル同様に、改行の代わりにセミコロン ; を用いても良い.

A = [ 1 3 8; 2 -5 10; -3 1 7 ]

また、改行とセミコロンを混ぜてもいい. やってみるとわかるが、このあたりは大変柔軟で、人間向きだ.

要素を具体的に与える前にまずは形をという場合は、
Array{型}(1次元目の要素数, 2次元目の要素数, $\cdots$)
もしくは
Array(型, 1次元目の要素数, 2次元目の要素数, $\cdots$)
とすることでも配列を作れる(後者の文法はいずれ廃止される予定). この場合、中身の値はてきと~に決まる.

次のような感じだ.

B = Array{Float64}(3, 4)

3×4 Array{Float64,2}:
 1.11778e-314  1.11778e-314  1.11781e-314  1.11781e-314
 1.11778e-314  1.11778e-314  1.11778e-314  1.11952e-314
 1.11778e-314  1.11778e-314  1.11781e-314  1.11993e-314

ちなみに、
zeros(型, 1次元目の要素数, 2次元目の要素数, $\cdots$)
とするとすべての要素がゼロの配列を作れる.

B = zeros(Float64, 3, 4)

3×4 Array{Float64,2}:
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0

型が Float64 でよければ、型の指定を省略してもいい.

B = zeros(2, 5)

2×5 Array{Float64,2}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

上の zeros の部分を ones とすると要素が全て 1 の配列が作れる.Float64 を省略できる点も同じ.

C = ones(Float64, 3, 4)

3×4 Array{Float64,2}:
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0
 1.0  1.0  1.0  1.0

配列の要素には、[要素番号] をつけるとアクセスできる. ちなみに番号は 1 からということになっている(要素番号を柔軟に扱いたければ、例えば OffsetArrays パッケージを用いよう).

だから

a[3] = 8
a

とすると

4-element Array{Int64,1}:
 1
 5
 8
 4

というように書き換わる.

配列から、一部分を取り出すことが出来る. 例えば、上の A に対して

A[1, :]

3-element Array{Int64,1}:
 1
 3
 8

というように 1行目が取り出せるし、また、

A[:, 2]

3-element Array{Int64,1}:
 3
-5
 1

というように 2列目を取り出すことも出来る.

範囲(詳細は後述するが、n:m と書くと、n,n+1,…,m という範囲を意味する)を指定して、

A[1:2, 2:3]

2×2 Array{Int64,2}:
  3   8
 -5  10

というように切り出すことも出来る.

また、例えば一次元配列ならば push! (最後尾に要素を追加), pop! (最後の要素を除去), unshift! (一番前に要素を追加), shift! (最初の要素を除去) という命令などで要素数を増減できるし、 二次元配列ならば vcat (下側にベクトルを追加する), hcat (右側にベクトルを追加する) という命令がある.

また、配列はいわゆる「ポインタ渡し」なので、コピーには注意がいる. 基本的には、次のように理解しておくと良いだろう.

  • b = a とすると、$b$ の実体が $a$ の実体そのものを指すようになる.つまり、名前が違うだけで実体は 1つ.上手に使えばいろいろ便利だが、混乱のもとでもあるので、よくわかっていない初学者はこうしない方がいい.
  • b = copy(a) とすると、$a$ のコピーが新しく作られて、それが $b$ となる.$a$ と $b$ は要素の中身がたまたま同じだけの別の変数ということになる.初学者はいつもこうしておくぐらいでいいだろう.
  • b = similar(a) とすると、$a$ と同じ型、大きさの配列(等)が新しく作られてそれが $b$ となる.便利なので覚えておこう.
  • $a$, $b$ が既に存在するとき b .= a とすると、$b$ の要素それぞれに $a$ の対応する要素がコピーされる.この場合、既にある $b$ のポインタが変わらずに済むので、この操作が必須のシーンも有る.

generator, 内包式, 範囲

配列の中身を定義するときに、何かを列挙する数学的な「式」= generator を使うことが出来る. この generator というやつは大変便利だし、直感的に書けるので重宝する.

書き方は
要素の式 for 動く変数 in 範囲
という感じで、例を見たほうが早いだろう. たとえば、generator として
3.1*n for n in 1:10
というものを使って、次のように配列を作ってみると何が起きているか分かるだろう.

a = [ 3.1*n for n in 1:10 ]

10-element Array{Float64,1}:
  3.1
  6.2
  9.3
 12.4
 15.5
 18.6
 21.7
 24.8
 27.9
 31.0

直感どおり、動く変数をカンマで区切って多重にすると、多次元配列になる.

B = [ 3.1*x + y for x in 1:3, y in 1:4 ]

3×4 Array{Float64,2}:
  4.1   5.1   6.1   7.1
  7.2   8.2   9.2  10.2
 10.3  11.3  12.3  13.3

カンマで区切らずに for を重ねると、多重ループをつぶした一次元的結果になる.

b = [ 3.1*x + y for x in 1:3 for y in 1:4 ]

12-element Array{Float64,1}:
  4.1
  5.1
  6.1
  7.1
  7.2
  8.2
  9.2
 10.2
 10.3
 11.3
 12.3
 13.3

さらに、generator の後ろに if 節をつけることで、フィルタリングも出来る.

c = [ 3.1*x + y for x in 1:3 for y in 1:4 if x+y == 4 ]

3-element Array{Float64,1}:
  6.1
  8.2
 10.3

さらにさらに(どこまでいくのか)、generator はあくまで「列挙するための式」なので、配列以外にも使える. 分かりやすいのは単なる合計の sum() などで、

sum(3.1*n for n in 1:10)

170.5

という感じになる.

ちなみに(上にも書いたが)、数字:数字 の形式の 1:10 は範囲で、{1, 2, …, 10} を意味する. さらに 数字1:数字2:数字3 の形式で書くと、”数字1 から「数字2 おきに」数字3 まで” を意味する. だから、

r = 1:0.3:2

とするとこの r は {1.0, 1.3, 1.6, 1.9} 相当になる. ちなみに範囲を配列に変換したければ, [ a for a in r ] としても良いけれどももっと簡単に collect 命令が使える.

collect(r)

4-element Array{Float64,1}:
 1.0
 1.3
 1.6
 1.9

さらにちなみに、範囲と似ているが違うものとして、linspace(数字1, 数字2, 個数) とすると、”数字1 から数字2 まで均等に 個数 だけ数を用意する” ものがある.

m = linspace(1,2,4)

4-element LinSpace{Float64}:
 1.0,1.33333,1.66667,2.0

これも範囲に似た使い方ができて、また、collect() で配列に変換が可能である. ただしこちらは要素を具体的に計算してしまうため、範囲に比べると計算量が多いことには留意しよう.

線形代数

ついでに線形代数の話も少し書いておこう. まず、単純な操作を書くと、

  • eye(n) とすると、$n\times n$ サイズの単位行列を作ることができる.
  • 行列 A に対し、その転置行列は A' と書けば良い.
  • 行列、ベクトル間の積は普通に * で書けば良い.
  • ベクトル a, b 間の内積は dot(a,b)
  • 行列 A, ベクトル x, b に対して、連立一次方程式 $A x = b$ の解 $x$ を求めるには x = A \ b とすれば良い(左から割る).解法は、通常は LU 分解が使われる.
  • 正方行列 A の固有値問題は eig(A) で解ける.固有値だけなら、eigvals(A), 固有ベクトルだけなら eigvecs(A) とする.
  • 逆行列は inv(A) で良いが、なるべく使わずにすまそう.
  • 他にも、行列式 det() やトレース trace(), さらに行列の指数関数 expm() や対数関数 logm() なども使える.ちなみに、exp()log() は要素それぞれに単純に関数を作用させるだけなので要注意だ.
expm(A) # exp(A) は欲しいものとおそらく違うだろうから、気をつけろ.

3×3 Array{Float64,2}:
 15.1577  -27.5533    -245.705
 39.7723  -12.0108    -173.023
 70.3364    0.140079  -138.679


map, broadcast

配列のような大きな、もしくは複雑な存在の要素全てに、それぞれ個々に同じ関数を作用させることが、ループ文を書かずに簡単にできる. その一つが map()命令で、使い方は例えば次のような感じだ.

map( sin, [1, 3, 7] )

3-element Array{Float64,1}:
 0.841471
 0.14112 
 0.656987

もう一つが、broadcast で、例えば同じ問題なら、

sin.( [1, 3, 7] )

などとする(途中の ピリオド が重要). これは、本来の文法 broadcast(sin, [1,3,7]) に翻訳されて実行される.

map は単なる個々の要素への適用だが、後者の broadcast はベクトル計算を意識しているようなので、一般には broadcast を使ったほうがよさそうだ(つまり、上の例だと sin.([1,3,7]) の方が良さそう).

ちなみに、こうした記法を使うようにすると、プログラムが本来の数式に近いものに似てくるので、全体の見通しが格段に良くなる. だから、どんどん使っていこう.


関数

自分で関数を定義する方法は複数あり、それぞれに特徴があるので、使い分けると良いだろう.

真面目に function を使う

例えば次のような感じ.ちなみに、返り値は return で.

function s3(x)
  return sin(x)^3
end

ちなみに、引数変数の型に縛りをいれたければ、

function sI(n::Int64)
  return sin(n)^3 + 4
end

というように、引数の後ろに ::型 という記述をつければ良い.

実はここが Julia の面白く、かつ、高速化等に大変に寄与しているところで、引数の型が違う関数は名前が同じでも別の定義ができる (multiple dispatch) のである. 例えば、次のようなことが可能だ.

function ss(x::Float64) # ss という関数で、引数は浮動小数
    return 1.5*x
end

function ss(n::Int64)   # 同じ名前の ss という関数だが、引数は整数
    return 3*n
end

こうすると、次のような動作をする.

ss(3.0001), ss(3) # 同じ関数をほぼ同じ値で計算してみるが、

(4.5001500000000005,9) # 入力値の型によって定義が異なるので、結果は全然関係ないものに.

この多重定義が可能な機能は、プログラムの記述の「良さ」を大変に改善するので、積極的に使っていきたい.

さらにまた、関数の返り値の型を制限したければ、関数名()の後ろに ::型 という記述をつければ良い. 例えば、次のような感じだ.

function f(x)::Float64
    return x^2
end

こうすると、次のような動作をする.

f(3.0), f(3) # 3.0^2 = 9.0 だが、3^2 = 9 なので、型指定がなければ結果は異なるはず.

(9.0,9.0) # 型指定のおかげで、いずれにせよ Float64 で結果が返る.


関数名(引数) = 関数式 として定義してしまう

例えば次のような感じ.単純な式にはこれがいいだろう.

s4(x) = sin(x)^4


無名関数として 引数 -> 出力式 として定義してしまう

無名関数とは、その名の通り、名前をつけずに定義して使える関数だ(名前をつけてもいい). なにか「作用」を記述する時など、これを使うと分かりやすくてよいケースが有る(また、プログラミングテクニックとしてほぼ必須のシーンも有る). たとえば、map() を使うときなどの、次のような感じだ.

map( x -> sin(x)^5, [1, 3, 7] ) # x->sin(x)^5 という関数を、直接、かつ、名前をつけずに定義して使っている.

3-element Array{Float64,1}:
 0.421887  
 5.59684e-5
 0.1224


制御構文

もちろん、普通のプログラミング言語同様に、制御構文もある. かなりシンプルに書けるように、よく考えて定義されている. 単純なので、簡単に紹介だけしておこう.

if 構文

例を挙げておこう.

if x < y
  println("x < y")
elseif x > y
  println("x > y")
else
  println("x = y")
end

また、a ? b : c という三項演算子がシンタックスシュガーとして定義されていて、これは

if a 
  b
  else
  c
end

と等価である.

さらに、ifelse 関数という、機能はそっくりの関数がある.しかしこれは中に分岐を含まない形で JIT コンパイルできるので、その分、(場合によっては)高速化が可能な文法になっている.詳しくは、いずれべつのところで解説しよう.

なお、複数の条件を and/or でつなぐには、&& (and), || (or) で繋げば良いのだが、Julia は数学的な表現ならばそのまままとめることが可能で、例えば、
条件式 x < y && y < z の代わりに x < y < z と書いても良い

for 構文

generator で使ったのでだいたいわかると思うが、次のような感じだ.

for n in 1:4
  println(n)
end

1
2
3
4

Julia は近代言語なので、in の後ろにくるのは、範囲でなくても列挙できるものならなんでも良い. 配列やタプル、その他、集合的なものならだいたい大丈夫で、例えば次のようなことも出来る.

tpl = (3, 4.1, "abc", -8)
for n in tpl
    println(n)
end

3
4.1
abc
-8

なお、for や後述の while などのループ文の途中で break を実行すると、その break を囲む上側で一番近い while を脱出する.

while 構文

これも簡単なので、例で紹介しよう.

n = 1

while n <= 5
  println(n)
  n += 1
  end

とすると、予想に違わず

1
2
3
4
5

という出力になる.


ファイルの読み書き

テキストの読み書きの一番シンプルな方法を紹介しておこう. これは通常の言語と同様のやりかたで読み書きするもので、

  1. ファイルをオープン
  2. 読み書き
  3. ファイルをクローズ

という3つのやり方を知っていればいいということになる.

まず、ファイルのオープンとクローズだけれども、オープンには、open(ファイル名, モード) というコマンドを使う. この出力が IOstream というものになるので、これに名前をつけておいて(ストリーム名)、読み書きやクローズの対象指定にする. モードには、だいたい次のようなものがある. まあ割り切って rwa を使うことが多いかな.

mode 意味
r read
r+ read, write
w write, create, truncate
w+ read, write, create, truncate
a write, create, append
a+ read, write, create, append

読み書きには大変多くのコマンドがあるけれども、ベタで一行ずつやるなら

読み … readline(ストリーム名)
書き … println(ストリーム名, 書き込む内容)

のコマンドでいいんじゃないかな.

そして、クローズには、 close(ストリーム名) とすればいい.

サンプルとしては、次のような感じかな.

# ファイルを読むだけ、のサンプル.

fn = open("dummy.dat", "r") # 読み込みモードでファイルを開く

while !eof(fn)  # eof(ストリーム名) はその読み込みが終端に達したら true になる
    line = readline(fn) # 1行読み込む
    println( line )     # 画面に出してみよう
end

close(fn) # ファイルを閉じる
# ファイルに乱数を 10行(1個/行)書き込むだけ、のサンプル.

fn = open("dummy.dat", "w") # 書き込みモードでファイルを開く

for i in 1:10
    println( fn, randn() )     # 乱数を一つ/行、書き込む
end

close(fn) # ファイルを閉じる
     << (newer post) (older post) >>