なぜ僕の中でElixirが一番であり続けるのか

March 30, 2020

僕は去年の 4 月(2019 年)に Elixir に出会ってそこからほぼ一年 Elixir と苦楽を共にしてきました。

僕のTwitterを見ている方なら、ぼやっと僕のことを Elixir の人と認識している人が多いかなって思います(FF の方と会った時の反応などで) 正直 Elixir はマイナーな言語ではあるので、そういった意味で印象に残りやすいのだと思います。

この一年、業務+家に帰ってからの趣味プログラミングでもずっと Elixir という感じでした。

Elixir 以外の言語の勉強にも勤しんでいますが、 Scala を勉強していても、Python を勉強していても、Ruby を勉強していてもやはり僕はElixir が一番好きだと思ってしまう訳です。 そこまでの魅力が Elixir にはあります。

今回この記事では僕の思う Elixir の持つ魅力(もはや魔力)のさわりを皆さんに紹介します

ツッコミ等あれば優しく(←最重要)コメント欄もしくはTwitterまでお願いします。

一般的に紹介されるElixirの魅力

Elixir には

  • 信頼性の高い”Erlang VM”の元で動作する
  • 並列処理に強い

などの魅力があると一般的に紹介されます。

ほーーん…なるほど… 正直言語によって性能大きく違いが出るほどの大規模なアプリケーションの作成の経験のない僕にはどちらもそこまで実感のあるものではありません

僕が感じたElixirの魅力

では僕が Elixir をここまでこよなく愛する理由はなんでしょうか?

  • パイプライン演算子
  • パターンマッチ
  • 関数型

僕はこの 3 つが Elixir の圧倒的魅力・中毒性を引き出していると思います。 主に前半の 2 つに関して紹介していきます。

まずは「パイプライン演算子」「パターンマッチ」とは何かをかるーく紹介します。

(追記) 公式では pipe operator と呼ばれているようですが、一般でも パイプライン演算子 と呼ばれることが多いような気がするので、この記事では パイプライン演算子 と統一して呼ぶことにします

パイプライン演算子

"text!" |> IO.puts()  #text!と出力される

上記の |> がパイプライン演算子です。 御察しの通り左の式で評価された内容を右の関数の第一引数として渡すという機能を持ちます。

パターンマッチ

※パターンマッチの説明は Elixir の根元に近ずくものでこの記事では深掘りません

map = %{foo: 1, bar:2}
%{foo: hoge} = map
IO.puts(hoge)          #1が出力される

もしかすると「ん?何が起こったの??」と感じる方もいるかもしれないです。 2 行目で行われているのがパターンマッチです1 簡単に説明すると map の内 foo の key をもつ value を新たな変数 hoge に束縛しています

パターンマッチ辺りを深く知りたい方は以下の記事が役にたつと思います。 [翻訳] Elixirのパターンマッチング

パイプライン演算子とパターンマッチを用いた関数設計

パイプライン演算子により人々(少なくとも僕)はようやくデータフローを意識できるようになります。

関数というものは基本的にデータを入れたら別のデータが帰ってくるもの(=データの流れ)です。

これを意識しつつ、例として何か 1 つの関数を書いてみたいと思います。

  • 特定のタグがついている記事を投稿日順で取ってきたい
  • タグには(なぜか)正規 ID と省略 ID という概念がある

という実際にありそうな get_with_tags という関数を書きたいとします。 タグの名前とを引数として渡す設定にします2

※ここから説明する関数の設計方法は僕個人の考えなので、他の方みんなこの方法を使っている訳でも、これがベストプラクティス!という訳でもないと思います。

関数の全体像を掴む

まずは大元の流れを掴みます。

  def get_with_tags(tag_name, author_name) do
    tag_id = Tag.get_id_by_name(tag_name, :canonical)
    
    get_posts_query()
    |> filter_by_tag_id(tag_id)
    |> order_by(asc: :created_at)
    |> Repo.all()
  end

この関数定義は大きく 2 つのセクションに分かれます

tag_id = Tag.get_id_by_name(tag_name, :canonical)

まず上記の部分でメインの処理に必要なものを用意します。 今回は正規の Tag の id が必要なのでそれを用意しています。

get_posts_query()
|> filter_by_tag_id(tag_id)
|> order_by(asc: :created_at)
|> Repo.all()                    #ElixirのEcto.Repoモジュールの関数

そして、上記がメインの処理です。 パイプライン演算子により、データを処理をしていく様子が関数名から追うことができるように設計します。

  • posts_query が
  • tag_id で絞られて
  • created_at の昇順で並べられて
  • Repo.all()される

というデータが加工されていく様子がパッとみてつかめるのでは無いでしょうか

もちろんこの段階では filter_by_tag_id などの関数は実際には定義していません。この段階で処理を関数名だけで読めるようにこのような関数が必要そうだなというものをざっくりパイプライン演算子で並べていきます。

またこの全体像の作成で、意識すべきは メインの処理以外に長いパイプライン演算子が必要となる場合は別関数として切り出すべき ということです。

これにより、1 つ 1 つの関数を出来るだけシンプルなものにできます。

実際に関数を作成していく

get_with_tagsの設計上で出てきた関数を定義していきます。

以下の order_by の定義をみてください。

  def order_by(asc: params) do
    #paramsを基準に昇順に並び替える
  end

  def order_by(desc: params) do
    #paramsを基準に降順に並び替える
  end

ここではパターンマッチが利用されています。 同じ関数が二度定義されているのは、(key: value とすると)

  • key の部分に asc が使用された時
  • key の部分に desc が使用された時

で関数の処理を変えるためです。3 4

先ほどの

長いパイプライン演算子は別関数として切り出すべき

ということを意識すると関数の処理のほとんどを大きな if 文で囲うことで条件分岐させるというような必要が出てきてしまいます。

そこにパターンマッチを用いることで、関数内で大きな if 文を用いて処理を切り分けるなどの必要がなくなり、関数定義自体の可読性が上がることが分かるかと思います。

そして以下が他の関数の定義も含めたこの関数の全体像になります。 Tag モジュールの方にも先ほど紹介した関数定義にパターンマッチを用いるテクニックが使われています。

defmodule Post do
  import Ecto.Query

  alias Ecto.Repo

  def get_with_tags(tag_name, author_name) do
    tag_id = Tag.get_id_by_name(tag_name, :canonical)
    
    get_posts_query()
    |> filter_by_tag_id(tag_id)
    |> order_by(asc: :created_at)
    |> Repo.all()
  end

  def get_posts_query() do
    from p in Posts
  end

  def filter_by_tag_id(posts, tag_id) do
    from p in ^posts, where p.tag_id == ^tag_id
  end

  def order_by(asc: params) do
    #paramsを基準に昇順に並び替える
  end

  def order_by(desc: params) do
    #paramsを基準に降順に並び替える
  end
end
defmodule Tag do
  def get_id_by_name(name, :cautional) do
    #Tagの名前をもとにTagの正規IDを取得する関数
  end
  def get_id_by_name(name, :summarized) do
    #Tagの名前をもとにTagの省略IDを取得する関数
  end
end

すなわちこの例で何が言いたかったのか

このように設計を行うことで結果として可読性が上がっているような気がしませんか?

  • 関数内のメインとなるパイプライン処理の関数名を見ればどのようなデータの処理が行われているか分かる
  • 小さめに切り分けられた関数定義自体もパターンマッチを用いることで可読性が上がる

これはパイプライン演算子とパターンマッチの両方がないと実現しないものであり、即ちElixir 特有の魅力に繋がっていると僕は思うわけです。

終わりに

いかがでしたか?? 特にパターンマッチのおかげで美しく設計できるような活用法は他にも沢山あり、この記事だけで到底紹介し切れるものではありません。 この記事で Elixir の魅力・魔力が少しでも伝わっていれば嬉しいです。

ちなみに現在 Elixir の他にパイプライン演算子を扱えるのは F#や R などくらいだそうです(安心安全のwikipedia調べ) 最近だと JavaScript や Ruby にもこのパイプライン演算子のようなものを導入しようという動きはあるそうですね。


  1. 実際には1行目で行われているものもパターンマッチです。Elixirは変数の代入に見えるものも内部的には全てパターンマッチが利用されています。が、深掘るとかなり細かい話になるので省略します。

  2. え、そもそもこの関数にidを渡さないような設計になってる時点で怪しいだろって?いい例が思いつかなかったんです…

  3. 今回のorder_byでは正直関数内にif文などの条件分岐を入れるだけでいいと思いますが、例なのでこれも許してください…

  4. Elixirが分かる方ならEcto.Query.order_byをわざわざなんでもう一回定義してるんだって感じだと思いますが、例が思いつかなかったのでこれまた許してください…

このエントリーをはてなブックマークに追加