5
4

More than 3 years have passed since last update.

juliaのススメ

Last updated at Posted at 2020-11-05

はじめに

みなさんは科学技術計算向けプログラミング言語「julia」をご存じでしょうか?すでに知っている人はその良さを広めましょう。そうじゃない人は是非juliaの良さを知って帰ってください。

この記事は以下の人を対象にしています。
・プログラミング初心者
・pythonやCなどほかの言語に飽きた人

なので細かい仕様などところどころ飛ばしながら書いています。プログラミングに十分慣れ親しんだ方には少し物足りないものになるかもしれません。

筆者は普段はpythonをメインにしていて、Cとscalaの経験がある程度です。pythonは楽ですけど飽きてきたので他の言語に触ってみたくなってjuliaを始めました。

juliaはすごい

開発チームはjuliaの長所を以下のように紹介しています。

We are greedy: we want more.
We want a language that's open source, with a liberal license. We want the speed of C with the dynamism of Ruby. We want a language that's homoiconic, with true macros like Lisp, but with obvious, familiar mathematical notation like Matlab. We want something as usable for general programming as Python, as easy for statistics as R, as natural for string processing as Perl, as powerful for linear algebra as Matlab, as good at gluing programs together as the shell. Something that is dirt simple to learn, yet keeps the most serious hackers happy. We want it interactive and we want it compiled.

要約すると
・オープンソース
・Rubyの動的さ
・Cのような速さ
・Lispのような真のマクロ
・Matlabの書きやすい数学表記、線形代数への強さ
・Pythonのような汎用性
・Rのような統計への使いやすさ
・Perlのような文字列処理
・対話的モードを持つ
・コンパイルできる

まあ簡単に言うと、いろんな言語のいいとこを取ったような感じです。

こうして挙げてみるとWe are greedyの通りかなり貪欲です。コードを書く前はC動的型付けでCのような速度は出ないだろうと思ってたんですが、少しコードを書いたら本当に早くて驚きました。

ここからは実際に使ってみて感じた感想を交えながらこれらの長所の一部を実際のコードを用いて紹介します。

初心者のために静的・動的について簡単に...

静的言語・動的言語とは大まかにいうと実行時に別途コンパイルが必要がどうかで分かれています。

静的言語の利点
・速い
・型(整数型や文字列型などの変数の種類)を書くので読みやすい
・実行前にエラー明らかなエラーがわかる(コンパイルエラー)
・メモリなどの最適化がしやすい

例:C/C++/Fortran/java/scalaなど

動的言語の利点
・簡潔に書ける
・実行が楽(コンパイルがいらないので)
・対話環境で簡単にコードのテストができる

例:python/julia/javascript/PHPなど

これだけ見ると静的言語のほうが優秀に見えますが、動的言語を好む人も多くいます。見れば型がわかるような変数にも型を書くのって結構面倒ですしね。

juliaは分類としては動的言語に属しますが、前述の通り、動的言語のメリットを残したまま静的言語の良い点を取り入れています。それではその実力を見ていきましょう。

速度

さてここからが本番です。

juliaはCのような速度を長所として挙げているのでCと、そして同じ動的言語のpythonとも比較してみます。

以下はpythonのコードですが、Cとjuliaでも同じ処理をしています。

import time
N = 100000
a = 0

start = time.time()
for _ in range(N):
    for _ in range(N):
        a = (a+1) % 12345
print(time.time() - start)

簡単な四則計算のテストです。

C 59.913835
julia 36.684635
python 1396.939088344574

pythonはこれだけの処理に20分以上かかっています。pythonのfor文が遅いのは有名ですが、はっきりと差が出てきました。
juliaは速度が速いことが大きな利点のCよりも単純な計算で上回っています。総合的にはCのほうが速いらしいですが、juliaは優秀な最適化によりCより早く動くときもあるみたいです。

def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

n=40として時間を計測しました。

C 1.755219
julia 0.782248
python 43.08912229537964

次はフィボナッチ数列の一般項を求める再帰のコードでテストです。メモ化などしてないので効率が非常に悪いですが、再帰呼び出しが何度も起こるのでこういったテストには最適です。
これもjuliaが最も速くなりました。

ちなみに、行列演算に関してはpython+numpyとjuliaがほぼ同速です。(Cは面倒だったのでやってません)

注意
juliaの速さはjitコンパイルに依存しています。ですので、実行時には処理を開始するまで少しラグがあります。このことからatcoderのようにプログラムを起動してからの経過時間で計測する場合、単純で軽い処理ではpythonにも劣るスピードになる可能性もあります。

書きやすさ

以下はGaussの消去法による線形連立方程式の解を求めるコードです。少し複雑になるので全体を軽く眺める程度で進んでもらえればいいと思います。
まずはjuliaから

using LinearAlgebra

function select_pivot(A, i)
    pivot = argmin(abs.(abs.(A[i:end, i]).-1)) + i-1
    if A[pivot, i] == 0
        throw(DomainError)
    else
        return pivot
    end
end

function swap_row(A, i, j)
    tmp = A[i, :]
    A[i, :] = A[j, :]
    A[j, :] = tmp
    return A
end

function gauss(A, b)

    N = size(A)[1]
    X = hcat(A, b)
    for i in 1:N
        pivot = select_pivot(X, i)
        if i != pivot
            X = swap_row(X, i, pivot)
        end
        println(X)
        X[i,:] = X[i,:] / X[i,i]
        m = X[i,i]

        for k in i+1:N
            X[k,:] = X[k,:] - X[k,i] * X[i,:] / m
        end
    end

    res = zeros(N)
    for i in 0:N-1
        res[end-i] = (X[end-i, end] - dot(X[end-i, begin:end-1], res))
    end
    return res
end

次にpython

import numpy as np

def select_pivot(A, i): 
        pivot = np.argmin(abs(abs(A[i:,i])-1)) + i
        if A[pivot, i] ==0:
            raise ValueError
        else:
            return pivot

def swap_row(A, i,j):
    tmp = np.copy(A[i,:])
    A[i,:] = A[j,:]
    A[j,:] = tmp
    return A

def gauss(A, b):

    N = A.shape[0]
    X = (np.c_[A,b]).astype("float64")


    for i in range(N):
        pivot = select_pivot(X,i)
        if i != pivot:
            X = swap_row(X, i, pivot)
        X[i,:] = X[i,:] / X[i,i]
        m = X[i,i]
        for k in range(i+1,N):
            X[k,:] = X[k,:] - X[k,i] * X[i,:] / m
    res = np.zeros(shape=b.shape)
    for i in range(1,N+1):
        res[-i] = (X[-i, -1] - np.dot(X[-i, :-1], res)) 

    return res

juliaの上記のコードでは引数に型を書いていません。(書くと型の推論にかかる時間が無くなるので高速化はします。)

簡単に書けることが大きなメリットであるpythonと比べてどうでしょうか?ほどんど同じようなコードで書けることがわかると思います。
juliaの目標の一つに科学技術計算を高速動作させ、かつ簡潔に書ける言語を目指しているというのがあります。そのため、juliaには標準で行列の演算に関する機能が多くされており、pythonのnumpyのような外部ライブラリに頼らなくてもよいのが魅力の一つです。

ここまでのまとめ

・Cとほぼ同速、時にはCより高速に動作する。
・pythonのように簡潔にコードが書ける

これだけでjuliaを選ぶ理由になると思いますが、juliaにはさらに強力な機能が備わっているので紹介します。

他言語のコードの利用

juliaではC/C++、Fortran、python、Matlabのコードを利用できます。ここではその方法は記載しませんが、既存のコードを利用できるというのは乗り換えるにあたって大きな利点です。
特にpythonについてはjuliaのjitコンパイルを利用することでオリジナルより高速に動作する場合もあります。

メモ:pythonを使うライブラリPyCallが呼び出すpythonは新たにjuliaによってインストールされたものになります。すでにインストールしているpythonを使いたい場合は.julia\packages\PyCall\BcTLp\deps\deps.jlをいじる必要があります。
もうpythonいらなくね

強力な型推論、パターンマッチ

型付けの強い言語はパターンマッチ等の強力な武器を使える代わりに変数宣言の度に型を記述するという面倒なことをしないといけません。a=1の時にわざわざa:Int = 1みたいに書きたい人はいないでしょう。
juliaでは型を明示的に書かなくても型を推論してくれます。でもこれはほとんどの動的言語に備わってる機能です。特段珍しいことではありません。しかし、juliaは型を明示的に書かなくてよいにも関わらずscalaやhaskellといった言語の特徴であったパターンマッチを利用することができます!

以下のコードでは(文字列、整数、(整数、整数))を要素に持つ構造体Characterに対してパターンマッチを行っています。

using Match

struct Character
    name::String
    height::Int
    birthday::Tuple{Int,Int}
end

patternmatch(chara::Character) = @match chara begin
    Character("sora",_ , _) => println("Found name:sora")
    Character(name, _ ,(11,_)) => println(name,"'s birthday is november")
    Character(_, _, _) => println("unkown character")
end

function main()
    sora = Character("sora", 156, (7,20))
    miyako = Character("miyako", 149, (11,2))
    yuno = Character("yuno", 174, (5,1))

    patternmatch(sora)
    patternmatch(miyako)
    patternmatch(yuno)
end

main()

出力

Found name:sora
miyako's birthday is november
unkown character

juliaではMatch.jlのマクロ@matchによってパターンマッチを実現しています。patternmatchは関数ですが、少し特殊な書きかたをしているので後ほど説明します。

なぜパターンマッチが使えると嬉しいのか
上のコードを見るとパターンマッチは単なる場合分けとして機能しているように見えます。しかし、上記の場合分けをif文で書いてみると以下のようになります。

function patternmatch2(chara::Character)
    if chara.name == "sora"
        println("Found name:sora")
    elseif chara.birthday[1] == 11
        println(chara.name,"'s birthday is november")
    else
        println("unkown character")
    end
end

if文の場合わざわざ構造体にアクセスしないといけないので煩雑になりやすいです。一方パターンマッチでは分岐の時に要素をとりだすこことができるので簡潔にコードを書くことができます。

数式に近い記法

ほとんどの言語では、関数の宣言を行うときdef hoge()とかfunction hoge()などと書くでしょう。これはjuliaも例外ではありません。しかしjuliaにはもう一つ別の関数定義の方法が存在します。

f(x)=2x^2+x^3\\
g(x)=sin(x)cos(x)\\

の2つの関数をpython、julia、Cで書いてみましょう。
python

import math

def f(x): return 2*x**2 + x**3
def g(x): return math.sin(x)*math.cos(x)

print(f(3))
print(g(math.pi/4))

出力

45
0.5000000000000001

julia

f(x) = 2x^2+x^3
g(x) = sin(x)cos(x)

println(f(3))
println(g(pi/4))

出力

45
0.5

C

#include <stdio.h>
#include <math.h>

float f(float x) {
  return 2* pow(x,2) + pow(x,3);
}

float g(float x) {
  return sin(x) * cos(x);
}

int main() {

  printf("%f\n",f(3));
  printf("%f\n",g(M_PI/4));
  return 0;
}

出力

45.000000
0.500000

細かい違いはあるにしてもこんな感じでしょうか。juliaは$f(x)=~$のように関数を定義することができ、通常の数式と同じように書くことができます。
また、$2×x$を$2x$と書けるのは素晴らしく、数字と記号の時にはわざわざ$2*x$なんて書く必要はありません。

これは私もどうしてそうなるのかわかりませんが、juliaは$g(\frac{π}{4})$の結果が厳密に0.5として返ってきてます。$sin(\frac{π}{4})$の時は普通に小数で返ってくるのでよくわかりませんが、厳密な値が計算できるように式を変換してるみたいですね。

書きやすさのところのコードを見てもらえればわかりますが、行列演算も直観的にかけます。また。通常の演算子・関数に.を付けることでユニバーサル化(ベクトル化)できます。

A = [1,2,3,4,5]
f(x) = 2x^2
println(f(A))

これはエラーになりますが...

A = [1,2,3,4,5]
f(x) = 2x^2
println(f.(A))

これはちゃんと計算できます。期待する動作は要素ごとに$f$を適用した配列が返ってくることでしょう。

出力

[2, 8, 18, 32, 50]

まとめ

なんかいろいろ書きましたが、結局何が言いたかったのかと言うと、

・juliaはいろんな言語のいいとこ取りしている。
・Cにも負けない程度の速度で動作できる。pythonの速度なんて比較にならない。
・型を書いても書かなくてもよい。パターンマッチが使える。
・全体的に簡潔に書ける。特に数式の記法に優れている
・ほかの言語のコードを流用できる。現在ライブラリの数は少ないが他言語の流用ができるので大きな問題にはならない

まとめるとこんな感じです。
公式サイトからjuliaをインストールするだけで簡単に始められるので、ぜひ一度試してみてください。



その他機能、高速化について

ここからはおまけです。

今回は紹介しませんでしたが、juliaは
・多重ディスパッチ
・メタプログラミングとlispライクなマクロ

を持っています。マクロによる遅延評価(というか式をすべてシンボルで保持している)は魅力的なので、慣れてきたらそっちの記事も書いてみようと思います。
これを利用してコードの自動生成もできるそうです。

juliaは初回実行が遅いと言われることがあります。これは事実で、処理の開始までにラグがあります。(あとライブラリの読み込みが遅い傾向にあります)
解決策として、
・REPLの利用
・PackageCompilerの利用

で大きく改善できます。
毎回julia hoge.jlをやるのは非効率で、
juliaで対話環境に入ったあと、include("hoge.jl")を繰り返すことで、コードが書き換わったところだけが再コンパイルされるので高速に実行できます。

またjitコンパイル形式なので、関数でまとめないと速度が出ません。重い処理を記述するときには気を付けましょう。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4