Elixir

Elixir 1.6のcode formatterで自動フォーマット

Elixir 1.6もそろそろリリースが視野に見えてきたところですが、1.6の注目機能といえばやはり自動フォーマット機能でしょう。
gofmtのように自動的にコーディングスタイルを変換してくれます。なんといっても言語公式のフォーマッタなので無駄な宗教戦争が起きることもなく平和的にコーディングスタイルを解決出来るし、コードレビューの手間も少なくなりいいことづくめですね。

2017-10-26現在ElixirのGitHub masterブランチをインストールすれば試すことが出来るので試してみます。

Environment

  • Elixir 1.6.0-dev (2017-10-26現在)

Installation

手っ取り早く1.6.0-devを試すためにkiex(Elixirのバージョンマネージャ)をインストールします

$ curl -sSL https://raw.githubusercontent.com/taylor/kiex/master/install | bash -s

.bashrc.zshrcでkiexを読み込むように以下を追加

test -s "$HOME/.kiex/scripts/kiex" && source "$HOME/.kiex/scripts/kiex

masterブランチの1.6.0-devをインストールします

$ kiex install master
$ kiex use master

iexを起動し1.6.0-devがインストールされていることを確認

$ iex
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

プロジェクト全体を一括でフォーマットするには.formatter.exsを作成し対象のファイルを指定します

[
  inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

フォーマット

$ mix format

.formatter.exsが存在する場合はプロジェクト全体のファイルがフォーマットされます。

ファイルがフォーマット済みかどうかは--check-formattedオプションで確認出来ます
フォーマット済みでないファイルがある場合は以下のようにエラーが返ります

$ mix format --check-formatted 
** (Mix) mix format failed due to --check-formatted.
The following files were not formatted:

  * config/config.exs

$ echo $?
1

フォーマット済みでないファイルがある場合はCIで落とすようにすれば、コードレビューでのコーディングスタイルの指摘を省略して本質的な部分に集中出来ますね

フォーマッタの気持ちを察する

最初はフォーマッタにかけると複数行になったり逆に見辛くなる箇所があります。
そういうところはフォーマッタの気持ちを察してフォーマット後も見やすくなるように書きなおすと案外構造が明確になって綺麗になったりします。
この点に関してElixir作者のJoséがリファクタリングの例を以下のissueで説明しているので簡単に詳解します
https://github.com/elixir-lang/elixir/issues/6643

複数行のデータ構造

例えば以下のようなコードをフォーマッタにかけると

:ets.insert(table, {doc_tuple, line, kind,
                    merge_signatures(current_sign, signature, 1),
                    if(is_nil(doc), do: current_doc, else: doc)})

以下のように変換されます。

:ets.insert(table, {
  doc_tuple,
  line,
  kind,
  merge_signatures(current_sign, signature, 1),
  if(is_nil(doc), do: current_doc, else: doc)
})

これは以下のようにインラインで実行している関数を外に出せば以下のように書けフォーマットにかけても変わりません

new_signature = merge_signatures(current_sign, signature, 1)
new_doc = if is_nil(doc), do: current_doc, else: doc
:ets.insert(table, {doc_tuple, line, kind, new_signature, new_doc})

結果的にインラインで実行されている関数を外部で実行することで構造が明確になったと思います

複数行の関数定義

例えば以下のようなコードをフォーマッタにかけると

def handle_call({:clean_up_process_data, parent_pid, child_pid}, _from,
                %{parent_pids: parent_pids, child_pids: child_pids} = state) do

以下のように変換されます

def handle_call(
      {:clean_up_process_data, parent_pid, child_pid},
      _from,
      %{parent_pids: parent_pids, child_pids: child_pids} = state
    ) do

これは関数ヘッダでの変数の展開を以下のように関数内で展開すれば関数定義は一行で済みます

def handle_call({:clean_up_process_data, parent_pid, child_pid}, _from, state) do
  %{parent_pids: parent_pids, child_pids: child_pids} = state

ヘッダでパターンマッチしたい場合もけっこうありますが1行が長くなりがちで可読性がだんだん悪くなってくるのでフォーマッタで複数行に変換されるレベルなら関数内で展開した方がいいのかもしれません

引数が多い場合

例えば以下のように引数が多い関数を使っている場合

assert_raise EEx.SyntaxError,
             "nofile:2: unexpected end of string, expected a closing '<% end %>'",
             fn ->
               EEx.compile_string("foo\n<% if true do %>")
             end

以下のように書き直すことでフォーマットされずに済みます

message = "nofile:2: unexpected end of string, expected a closing '<% end %>'"

assert_raise EEx.SyntaxError, message, fn ->
  EEx.compile_string("foo\n<% if true do %>")
end

messageに長い文字列リテラルを入れることでより明快なコードになった気がします

各エディタ用プラグイン

VSCode

Atom

まとめ

Elixir 1.6のフォーマット機能を簡単に使ってみました。
フォーマット後のコードが複数行になるなどしてくるとそれだけコードが複雑化してきているというコードスメルを可視化出来るという意味でフォーマッタは有用な機能だと思いました。
フォーマッタの気持ちを察してフォーマッタにフォーマットされないようなコードを書けるようになりたいですね