20
6

More than 1 year has passed since last update.

Python歴15年のエンジニアが、1年間Elixirに浸かってみた結果

Posted at

はじめに

2022年の6月に、stackoverflowのアンケートで、Phoenixがmost loved web frameworksでトップというニュースがありました。

Elixirのドキュメントを読みながら使い始めてみたのが、ちょうど一年前の、2022年の7月8日でした。

この一年間を振り返ってみると、Elixirがきっかけで、得られた事がいろいろあったので、まとめてみます。

関数型プログラミングを体験できた(7~9月)

手続き型のプログラミング言語でも、mapやreduceなどの命令があり、関数型的なプログラムを記述する事ができます。関数型には興味がり、JavaScriptで関数型的な書き方をした経験もありました。
しかし、Elixirを使って、関数型プログラミングを作った時に初めて、関数型プログラミングを実感できたと感じました。

英語学習で例えると、JavaScriptで関数型をやってみるのは、日本に暮らして、日本人と英語するようなものです。困ったら日本語で話せますし、語彙にないことは日本語で話しているような状況です。
一方、Elixirの場合、アメリカで一人で暮しながら、英語を話す状態です。関数型で書くしかないんです。関数型から逃げられません。関数型の場合の作法がわからない場合でも、逃げ道はありません。困った時、Elixirの場合、Hexdocsが充実していて、助かりました。

逃げ道はありません。と書きましたが、Elixirは手続きっぽく書くこともできたりして、ちょっとゆるい所があって、逃げ道もある緩い感じもあります。

Elixirが、関数プログラミング言語のネイティブだと思ったのは、次の3点です。

関数の定義が工夫されている

関数の定義に、ガード節とパターンマッチングが使えます。詳しくは、書きませんが、関数の内部でおこなう条件分岐をなくしてシンプルに書けます。
これがなくても別の記述方法で書けますが、便利です。
例えば、xのy乗を計算するpow(x,y)をパターンマッチングを使わないで書いた場合

    def pow(n,k), do
        if k==0 do
            1
        else
            n * pow(n, k-1)
        end

同じことを、パターンマッチングを使うと次のように記述できます。

    def pow(n,0), do: 1
    def pow(n,k), do: n * pow(n, k-1)

パターンマッチングとは、同名の複数の関数を記述してある場合、引数が、マッチしている関数が選択される仕組みです。

パターンマッチの記述は、次の数式とよく似た記述にできて、わかりやすいと思いました。

pow(n,k) = \begin{cases}
 1 & (k = 0) \\
 n * pow(n, k-1) & (k \neq 0)
\end{cases}

再帰の処理はよく使うので、助かります。
うまく書けると楽しい。

イミュータブルな操作提供されている

例えば、Pythonの辞書型の値を変更する場合、こんな感じで書き換えます

>>> users = {"jone":{"age":27}, "meg":{"age":23}}
>>> users["jone"]["age"] = 28
>>> users

ElixirのMap(Pythonの辞書にあたる)は、Pythonの様に値を書き換える事はできず、変更点を書き換えた複製を作る動作になっています。

users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}

users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
put_in(users["john"].age, 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}

この例は、Mapの中にMapがあり、Pythonで同様な処理をするには、deepcopyが必要ですが、deepcopyして値を変更したものが返されます。
MapやListを破壊する手段がないので、関数の呼び出しで副作用が生る事もありません。

パイプ演算子が楽しい

関数型プログラムだと関数の戻り値を次の関数の引数にすることはよくあります。

a = func3(func2(func1(x)))

わかりずらいので、変数を挟んで、次のようにかくかもしれません。

ret1 = func1(x)
ret2 = func2(ret1)
a = func3(ret2)

これを、パイプ演算子|>を使うと次のように見やすく記述ができます。

a =
x
|> func1()
|> func2()
|> func3()

このパイプ演算子の記述方法は楽しいので他の言語にもぜひ取り入れてもらいたいです。

このような、関数型プログラミングを支える仕組みを持った言語でプログラムを作ってみる事で、関数型プログラミングに専念できます。

機械学習のプログラムを作成できた(9月~1月)

言語を習得するには、実際に使ってみるのが一番です。
ゼロから作るDeep Learing4を読んでいたので、この内容をElixirで記述してみました。

Elixirが、機械学習のような処理にも対応できる言語であったことで、五目並の対戦相手を強化学習で作成し、実際にゲームができるものを作ってみる事ができました。
強化学習の基礎を理解することができました。
詳しくは、次の記事に参照してください。

AtCoderをはじめる(2月~7月)

ElixirのTwitterやQiitaの記事に、AtCoderをElixirでやってみてよかった的な書き込みがあり、AtCoderをやってみました。
やってみると、競技プログラミングがどんなものかわかってきました。
AtCoderが求めているプログラミング力って、実用的なものもあって、ちょっとやってみようかな。と思いました。

Elixirで解けるか?

Elixirでも解けました。
問題になりそうなのは、Pythonだと処理時間が間に合わず、PyPyで実行すれば間に合う様な、時間が厳しい問題の場合、Elixirでも厳しいと思います。
初心者の私が解く問題は問題ありませんでした。
しかし、簡単な問題は、Pythonで書いたほうが簡単(私がElixirに慣に慣れてないので)でコーディングも速くできました。なので一旦Pythonで始めて、Elixirに適した問題はElixirでと解くことにしました。

AtCoderをやってみて得られたこと

  • 忘れかけていたPythonを思い出せた
  • Elixir/Pythonのプログラムを書くのが速くなった
  • Elixirでパターンマッチングや再帰を使った記述に慣れた
  • Pythonで書くより、Elixirで書いたほうがよい場合が自分なりにわかってきた

Elixirで書いたほうがわかりやすかった例

DP (動的計画法) は慣れてないので一苦労です。
例えば次の問題
問題
公式解説

解説にあるDPの説明と同じ事を、Elixirで再帰関数で書いてみました。この問題については、Elixirのプログラムの方が、理解しやすく、作りやすいとおもいました。
この問題は、計算量が大きいので、Memo化(一度計算した値は、記録してある値を返すだけする)を:etsというメモリを使って行い、正答できました。

    def range(s,e,step) do
        for x <- 0..div(e-s,2), do: x * 2 + s
    end

    def cost(a, l, r) when r-l == 1 do
        abs(elem(a,l) - elem(a,r))
    end

    def cost(a, l, r) when rem(r-l,2)==1 do
        case get({l,r}) do
            nil ->
                cost1 = cost(a,l+1,r-1) + abs(elem(a,l) - elem(a,r))
                cost2 =
                    range(l+1, r-2, 2)
                    |> Enum.map(fn x -> cost(a,l,x)+cost(a,x+1,r)end)
                    |> Enum.min()
                ret = min(cost1, cost2)
                put({l,r}, ret)
                ret
            x -> x
        end
    end

    def new() do
        :ets.new(:memo, [:set, :protected, :named_table])
    end

    def put(k,v) do
        :ets.insert(:memo, {k,v})
    end

    def get(k) do
        case :ets.lookup(:memo, k) do
            [{_,v}|_] -> v
            [] -> nil
        end
    end

    def solve(n, a) do
        new()
        ans = cost(List.to_tuple(a),0,2*n-1)
        IO.puts(ans)
    end

単体テストの考えに役立った(6月~7月)

単体テストの考え方/使い方を読みました。
この本は、どのようにすればUnitTestが行いやすいプログラムにできるかが説明されていました。
関数は、UnitTestでテストしやすいので、関数で記述できる部分と、関数では記述できないような処理を分離することが大切になります。

分類 関数で記述できるか? テスト方法
ドメイン・モデル/アルゴリズム 関数で記述できる 単体テスト
コントローラ 関数だけでは記述できない
ミュータブルな操作
統合テスト

この書籍では、単体テストを行いやすように、どのように分離すれば良いかが説明されています。Elixirのプログラムの場合、

  • ドメイン・モデル/アルゴリズムの部分は、自分で作成するプログラム
  • コントローラにあたる部分は、GenServerであったり、Phoenixのフレームワークによって提供されている

にあたりそうです。
Elixirで作っていたものが、理想的な状態で、関数型プログラミング言語に触れていた事が理解の助けになりました。

まとめ

  • パラダイムの異なる言語を習得してみると、いろいろ得られるものがあった。
  • Elixirは、始めやすく、実用的であった。
  • Elixirを1年使い続けると他の言語に戻れなくなります。

Elixir初めて知ったという方、この機会に試してみてください。

参考

20
6
2

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
20
6