Elixirにコントリビュートしてみよう!

最近ElixirにPRをだしてマージされました。

github.com

Elixir 1.10からEnum.frequencies/1Enum.frequencies_by/2が多分生えます。

       iex> Enum.frequencies(~w{ant buffalo ant ant buffalo dingo})
      %{"ant" => 3, "buffalo" => 2, "dingo" => 1}

      iex> Enum.frequencies_by(~w{aaa aA bbb cc c}, &String.length/1)
      %{3 => 2, 2 => 2, 1 => 1}
 

着想はRubyのtallyからです。

Elixirは書いていてとても楽しい言語で,リンターやテスト等周辺のエコシステムも整っていて便利です。
みなさんもぜひElixirの魅力にはまってElixirにコントリビュートしてもらいたいということで記事を書きました。 今回はElixirという言語そのものの仕様ではなく、elixir-lang/elixirへの実際にPRした時の流れを書きたいと思います。 後世のElixirを担うそこのあなたに少しでも役にたてば幸いです。

elixir-lang/elixirの特徴

コントリビュートするという視点で見てelixir-lang/elixirの一番大きな特徴は実装がElixirで書かれているということです。Rubyの実装に貢献するにはC言語の知識が必要ですがElixirではElixirが書ければElixirが書けます!

elixir-lang/elixirに貢献したい!どうすれば?

公式のREADMEを読みましょう。 一番最新で正しい情報がのってます。とはいえそれだとこの記事終わってしまうので自分のやったことの流れを書きます。

  1. forkしてローカルにclone
  2. Enjoy Cording!
  3. make compileして変更したexファイルをコンパイル
  4. bin/elixir lib/elixir/test/elixir/変更したファイルのテスト.exsでテストを実行
  5. mix formatでリンターを走らせます
  6. commit & push!

1,2,6はどのOSSでも当然ですね。 2の作業中に実際にREPLを叩きたい場合は、変更したファイルをコンパイルしてbin/iexで変更したコードが反映されたiexを起動することができます。
以下のように変更したファイルだけコンパイルすることもできます。
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
6については、例えばrails/railsだと「squashして一つにまとめてね」といった注意点があったりしますがElixirというOSSには今の所ないみたいです。

僕はできなかったけどやるべきだったこと

新機能の追加はまずメーリスで提案しよう!

いきなりREADMEちゃんと読め案件なんですが、新機能のPRを出す前にElixir Core maling listで提案してなぜ役立つのかをコミュニティに説明しましょう。僕はこれを怠りいきなりPRたてて30分でcloseされました。 今回の自分の提案では「tallyの機能って他の言語であったりする?」「名前をこうするのはどうだろう」と様々な意見が出ていました。コミュニティの熱量も感じられますしメーリングリストは怖いところではないですよ!

ちゃんと議論が収束するまで待ちましょう

これが一番の反省点なんですが、メーリングリストに提案を出してすこし肯定意見が出始めたあたりで勇んでPRを再度立ててすぐにcloseされるということがありました。ちゃんと議論の収束やコミッターからの指示を待ちましょう。

パフォーマンスを意識したコードを書こう

最初に僕が出した実装はこれです。

   def tally(enumerable, func \\ fn x -> x end) do
    group_by(enumerable, func)
    |> map(fn {key, val} -> {key, count(val)} end)
    |> Map.new()
  end
 

関数名とか可読性は別としてこの実装だとgroup_byを使っているので全ての要素に二回アクセスしていることになります。
PRを出して提案されたコードは以下

   def frequencies_by(enumerable, key_fun \\ fn x -> x end) when is_function(key_fun) do
    Enum.reduce(enumerable, %{}, fn entry, acc ->
      key = key_fun.(entry)
      Map.update(acc, key, 1, &(&1 + 1))
    end)
 

reduceは折りたたみ演算です。この実装なら各要素へのアクセスは一回ですみます。 全てのプログラミングにおいて当然のことですが、特にプログラミング言語や大きいOSSではパフォーマンスが重要だと思います。意識して書くと良いですね。

自分一人でPRをつくるわけではない

特に既にコミュニティがあるOSSに言えますが、PRは自分だけで完成するものではないです。 最終的に自分が出したPRも色々変更されています。 というか最終的な実装は僕が書いたとは言えない
最初から文句なしマージというのは他のPR見ていてもなかなかないと思います。(typo修正とかは別)
きちんとOSSとコミッターに敬意をはらったPRをだして反応に真摯に対応しましょう。

おわりに

後半はElixir関係なくなっちゃいましたが、ElixirにPR出すときの簡単な標識にでもなっていれば幸いです。
なんだか偉そうなこと書いたけどメンテナー側の人間ではないのでもしかしたら間違ってるかもしれない。とりあえずROMして雰囲気を掴むのが一番いいと思います。
もしElixirに興味を持たれた方は(日本語版は1.2時点のもので少々情報が古いですが) プログラミングElixirという本がおすすめです。というか神本なのでぜひ買ってください。他言語からElixirを学ぶ人向けに書かれていて、ライブラリを作るハンズオンや非同期処理やメタプログラミング等盛りだくさんの内容です。
また今回PRを出すに当たり#tokyoexで学んだことにとても助けられました。大変勉強になる会なのでみなさんもぜひ足を運んでみてはいかがしょうか。