LoginSignup
0
1

More than 1 year has passed since last update.

Julia入門メモ

Last updated at Posted at 2023-03-08

モチベーション

Clojureの実行速度だと困ることが多々あるので、Juliaを試している。

Pythonのnumpyで遅すぎるということはないが、オブジェクト指向が起因でデータ構造が入り乱れる (リストとpandasとnumpyのように) のが好みでないし、REPLをより高速に回せるのであればそれに越したことはない。Common Lispのリストならば単一のデータ構造で何でもできるが、Clojureと同様にやはり遅くなってしまう1

メモ

  • 文字列はダブルクオートで、シングルクオートは不可。

  • mapをゴリゴリ利用しても速い。Python/numpyでもここまで速くはないし、こうも自然にmapは利用できないはず。

    • 「①抽象データ型 (シーケンス) と②シーケンス操作関数だけで何でもやろう」「自作変数も自作関数も少なくて短い方が良い」というClojureのシンプル精神は、Julia上でもある程度までは実現可能に思う。イミュータブル・遅延評価は再現が難しい2けれど、無くても実用上は3そこまで問題にならず、速度とのトレードオフとしては良い選択。
    • バクを取るためにコードを書いているわけではないように、最適化をするためにコードを書いているわけではない。
    • map/filterはClojureと同じ感覚で使える。applyを使いたい場合はリスト内包表記でごまかす。
    • map内でif文を使いたいときは、do構文を利用して見た目を整える。
      • doは無名関数をmapの第一引数に渡す。filterにも使用できる。
      map([A, B, C]) do x
          if x < 0 && iseven(x)
              return 0
          elseif x == 0
              return 1
          else
              return x
          end
      end
      
    • 無名関数はx -> x + 3(fn [x] (+ x 3))
    • 引数を1つしか取らない関数については、スレッド的なことをするパイプ演算子がある12 |> x -> x + 3 #=> 15
    • 無名関数を使えば、複数の引数が必要な関数についてもパイプ演算子が使える。
      10 |> x -> Dict(:ten => x)
      # Dict{Symbol, Int64} with 1 entry:
      #   :ten => 10 
      
  • 関数を閉じるためにendを使う言語に忌避感を持っていたが、コード内で関数をまとめておけば、あまり気にならない。

  • デフォルト引数は、名前を書いて渡せない。Pythonと違うところ。

    function hoge(a, b=2; c=3)
        println(a, b, c)
    end
    hoge(1, 2) # 123 期待通り
    hoge(1, 3) # 133 期待通りでない
    hoge(1, 2, 3) # ERROR (キーワード引数cへ、名前を書かずに渡している)
    hoge(1, b=2) # ERROR (デフォルト引数bへ、名前を書いて渡している)
    hoge(1, c=3) # 123 期待通り
    hoge(1, 2, c=3) # 123 期待通り
    hoge(1, b=2, 3) # ERROR (デフォルト引数bへ、名前を書いて渡している)
    hoge(1, b=2, c=3) # ERROR (キーワード引数cへ、名前を書かずに渡している)
    hoge(1, b=2; 3) # ERROR (いずれにしろ、セミコロンより後ろは名前を書いて渡す必要がある)
    
  • 関数へのデータ渡しは、基本的に参照渡し (共有渡し)。

  • ifelse文は便利だが、分岐に関わらずtrue節・false節の両方が評価されてしまう。

  • or/andではなく、||/&&

  • while内でカウンタを進める際、配列は1始まりなので注意。

    a = [1, 2, 3, 4, 5]
    
    # こうすることが多いはず
    i = 1
    while i < stop
        print(a[i])
        i += 1
    end
    
    # これはエラー
    i = 0
    while i < stop
        print(a[i])
        i += 1
    end
    
  • 関数内でreturnを書かなくても、最後に評価される値が返される。関数型言語的。

  • 三項演算子は連結できる false ? 1 : false ? 2 : 3 #=> 3

  • データ型は下記 (参考)。

    • 数字 (整数、小数、複素数、分数)
    • Bool (true/false)
    • 文字 ('a', 'A')
    • 文字列 ("string"): *で文字列結合、$(var)で変数展開
      • 文字列→小数: parse(Float64, "3.14") #=> 3.14
      • 部分文字列: chop("string", head=1, tail=2) #=> "tri"
    • タブル: Pythonと同じ。
    • 辞書: Dict(key=>val)で生成。
      • Pythonの:と異なり、=>と2文字も打つのが手間。辞書のキーに文字列を使うと、ダブルクオートを打つのが手間。良いマナーかは調べが付かなかったが、シンボルをキーとしたほうが楽。
        d = Dict(:name => "Julia", :version => 1.8)
        d[:name] # "Julia"
        
    • 集合: Pythonと同じ。
    • 範囲オブジェクト: start:stopstart:step:stopの形式。
    • 配列
      • [1 2 3] (1×3 Matrix{Int64}) でも、[1; 2; 3] (3-element Vector{Int64})でも、[1, 2, 3] (3-element Vector{Int64})でも生成できる。

        • Vectorは Nx1 Matrix の別称。なので[1, 2] + [1, 2] == [2, 4]だが、[1 2] + [1, 2]は行列計算ができずにエラー。
      • 行列の積は[1 2] * [3, 4] == 11。行列の要素ごとの積 (アダマール積) は[1, 2] .* [3, 4] == [3, 8]のように.を付ける。

      • Pythonと異なり、スライスの終わりを行列サイズ以上にはできない。a = [1, 2, 3]; a[1:100]はエラー。

      • 配列は1始まりなので、Pythonと異なりstop-startの値が行列のサイズとは等しくならないa[2:4]には、値が3個入っている。

      • 例えば1次元と2次元どちらかの配列を列方向で半分に区切るような関数は、if文を使わずsizeを使い、常に2次元を扱うような切り出し方をする。

        • Pythonのようにint(1.5)でint型にはできない。JuliaでIntを使う際は、中の小数はx.0になっている必要がある。
        • このやり方をすると、Vector型を分けたときにMatrix Nx1型で返ってくる。実行速度に差異はあるのだろうが、今の所、実用上は問題を感じていない4。Vector型しか受け付けないような関数があった場合、vec()関数でVector型に戻すこともできる。
        function half(a)
            l = size(a)[1] / 2
            a[1:Int(ceil(l)), :], a[Int(ceil(l))+1:end, :]
        end
        half([1, 2, 3, 4, 5]) # ([1; 2; 3;;], [4; 5;;])
        half([1 2; 3 4; 5 6]) # ([1 2; 3 4], [5 6])
        half([1, 2, 3, 4, 5])[1] + [1, 1, 1]
        # 3×1 Matrix{Int64}:
        #  2
        #  3
        #  4
        
      • Pythonと異なり、配列スライスの終わりのインデックスが結果に含まれる5

        # Python
        a = [1, 2, 3, 4, 5]
        a[0:3], a[3:] # ([1, 2, 3], [4, 5])
        a[1:1] # []
        
        # Julia
        a = [1, 2, 3, 4, 5]
        a[1:3], a[4:end] # ([1, 2, 3], [4, 5])
        a[1:1] # 1
        
      • 配列が入った配列・タプルa = [[1, 2, 3], [4, 5, 6]]を二次元配列[1 2 3; 4 5 6]にするには、stack(a, dims=1)参考 (英語)

        • mapは結果を引数に合わせて配列かタプルに入れて返すので、処理した複数のベクタをstackでくっつけられる。
          map(x -> [x, x, x], [1, 2, 3])
          # 3-element Vector{Vector{Int64}}:
          #  [1, 1, 1]
          #  [2, 2, 2]
          #  [3, 3, 3]
          using DataFrames # あるいは、Julia 1.9からは標準で使えるらしい
          stack(map(x -> [x, x, x], [1, 2, 3]), dims=1)
          # 3×3 Matrix{Int64}:
          #  1  1  1
          #  2  2  2
          #  3  3  3
          
      • 配列の列ごと・行ごとの値の合計は、sum(a, dims=n)n=1で行の和、n=2で列の和。

      • 配列の最大値はmaximummaxではない。

  1. スタイルガイドでも、『リストの乱用はしない』ことが推奨されている。

  2. タプルとジェネレータ式で模倣できるように思われる。それにミュータブルがもたらす混乱については、破壊的な関数に!を付ける慣習だけで、(小規模なコードであれば) 対応できる。

  3. 哲学的には大問題かもしれない。

  4. 『パフォーマンスのためにベクトル化する必要がない。ベクトル化しないコードも速い』, Julia 1.0 ドキュメント, https://mnru.github.io/julia-doc-ja-v1.0/index.html

  5. そもそもPythonとJuliaで配列の数え方が違うのでこの言い方はおかしいけれど、こう言いたくなる。

0
1
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
0
1