はじめに
こんにちは。12/13のElixir Advent Calendarを書かせていただきます。
最初は面白いライブラリとか(docs周り)について書こうと思っていたのですが、僕のコードの書き方の変遷について書くことにしました。
最初のころは「パイプライン演算子のある、なんかイケてる関数型言語」っていうイメージと、クラロワをして当時よく遊んでいたので、馴染みのあるエリクサーという単語に興味を引かれて始めました。それまでパターンマッチや再帰を使いこなせたことがなかった僕にとって、Elixir風の書き方を試していくことはとても斬新な感じがして楽しく思いました。
最初から上手には全然書けていないし、今でも直せそうなところはたくさんあると思いますが、これを読んでElixirに興味を持ってくれる方が一人でも増えたら嬉しいです。
それでは、前の自分を思い出しながら書いていきます。
よくわかっていない期
はじめにの理由から、Elixirをやってみようと思いました。
「〇〇が作りたい」だったり、「関数型言語にちょっとでも触れてみたい」というわけではなく、エリクサーという単語に馴染みがあったのが大きかったです。
Elixirの文法をちょろっとやった後、何か動くものを作ってみようと思い、Phoenixを使ってチャットアプリケーションを作りました。当時の僕はまだ公式ドキュメントをちゃんと読んでいくことが出来なかったので、どこかの記事を参考にして作成しました。
次のようなコードを書いたりもしていました。
def search_id(conn, _) do
result = conn.params["word"]
|> Auth.search_id()
json(conn, formatuser(result))
end
当時の僕の、とりあえずパイプライン演算子が使ってみたかったという気持ちが感じられます。
「後続の関数の第一引数に値を渡す」のがパイプライン演算子の役割でしたが、Enum
モジュールやリストについての理解が浅かった当時は、使いみちがよくわかっていませんでした。
他にもよくわかっていなかった概念はすごくたくさんありました。
- チェンジセット
- チャンネル
- スキーマ(マイグレーションファイルもあるのになんでだろう?って思ってました)
- ピン演算子
- use require import(aliasはわかった)
- conn
- パターンマッチ
- with
HexDocsが理解できないので、Elixir Schoolや、これらのわからないところの説明をしてくれているサイトでちょっとずつぼんやりとイメージをつけていきました。(理解したとは言っていない)
そしてパイプライン演算子ってもっといい感じに使えないのかな、と考えていった結果、Enum
モジュールと出会いました。
Enum
の登場によって、僕のElixir理解は非常に深まりました。
Enum大好き期
この時期は、Enum
モジュールが使いたくて使いたくて仕方がありませんでした。
Enum
を使うとパイプライン演算子でつなげてリストに色々なことを行えます。
names = User
|> where([u], u.id == ^user_id)
|> Repo.all()
|> Enum.map(fn user ->
user.name
end)
このサンプルプログラムからもイメージしやすいように、データを次々と扱いやすいように変換したりする際はEnum
とパイプライン演算子の相性がとても良いです。
Enum
モジュールは今でもすごく気に入っていて、リストを扱う際にすごく頻繁に利用しています。
しかし、このときはEnum
とパイプライン演算子の相性の良さにただただ驚き、使えそうな場面では必ずと言っていいほどEnum
を使っていました。
Repo.all
とEnum
があればなんだってできる。
強大な力を手にしてしまったこの頃の自分はすべてのデータをパイプラインでつなぎたい衝動に駆られていくようになりました。
Enumを覚えていくと同時に、体がパイプラインの快感を求めていくようになってしまいました。
そんな状況で、自分の中に一つの疑問が浮かびました。
「パイプラインの中にKernelの演算子を入れてしまってもいいのだろうか?」
User
|> where([u], u.id == ^user_id)
|> Repo.all()
|> length()
|> Kernel.==(10)
|> assert()
例えばこのような感じです。これが許されるのなら、もう何者も僕のパイプラインを妨げることは出来ません。
パイプライン信仰期
多少の関数分けはしつつも、すべての関数をパイプラインでつなげるために作ってしまう時期に突入しました。
このころのコードが一番見づらかったです。
お見せできるプログラムがもう残っていないのですが、思い出して書いてみるとこんな感じです。
User
|> where([u], u.id == ^user_id)
|> Repo.all()
|> length()
|> Kernel.==(10)
|> if do
Company
|> where([c], c.id == ^company_id)
|> Repo.all()
|> Enum.map(&Map.get(&1, :name))
|> Enum.reject(&is_nil(&1))
else
[]
end
この頃にif
やcase
にパイプライン演算子で値を提供できることを知り、衝撃を受けました。
パイプラインでつなげるためにそれらの条件分岐を多用し、条件分岐のためにboolean
を返すような処理をパイプラインの中に挟んでいました。
また、ここでは書いていませんが、~>
演算子を定義して、パイプライン最中での変数束縛も可能にしていました。
参考
https://www.reddit.com/r/elixir/comments/7j2lx2/pipe_a_functions_output_into_a_variable/
この長い長いパイプラインを書いているときは、まるでぷよぷよの連鎖のように処理が繋がっていくので、非常に気持ちが良かったです。特に、テストをするために〇〇Test
モジュールで、巨大なパイプライン連鎖を生み出しまくっていました。
そんなある日、久しぶりにリーダブルコードを読み返してみたら、次のように書いてありました。
「読みやすい」コードを書くのに「面白い」ことをする必要はない。
これは今の自分に最も欠けている感覚だ!
パイプライン演算子に取り憑かれてしまっていた自分を反省して、自分がそれまでに生み出してきた「書くときは気持ちいいけどメンテナンスのしづらいコード」を1つずつ直していくことにしました。
やはりプログラムは読みやすくなくては。
パターンマッチ・defp期
どうすれば読みやすいコードが書けるのかを知るために、Githubで他の人のコードを色々と見たりしました。
この頃はもう始めたての頃と比べると格段にコードを追えるようになっていたので、読んでいく上での苦労は最初よりも少なくなっていました。
他人のコードを読むことは、自分にとって非常に勉強になりました。中にはwith
をスッキリと書くためにOKというライブラリを入れている人がいたり、ちゃんとケースに応じてKernel
の関数をパイプラインの中に組み込んでいる人がいたりしたことが印象に残っています。
ここで僕は新しいことをいくつか知りました。
- エラーハンドリングのしやすいコードを書こう
- パターンマッチを積極的に使ってネストを減らそう
- defp関数を適切に使って関数分けをしよう
- よく使う構造のマップは、構造体として定義して名前を付けてあげよう
- etc
リーダブルコードに救われました。
現在
現在も、過去のパイプライン狂だった自分を反省して読みやすいコードを心がけて書いています。
@doc """
Check if the given instance name exists.
"""
def instance_exists?(instance_name, db_index) do
conn = conn()
with {:ok, _} <- Redix.command(conn, ["SELECT", db_index]),
{:ok, val} <- Redix.command(conn, ["GET", instance_string_key(instance_name)]) do
!is_nil(val)
else
_ -> false
end
end
今の自分の課題は
- しっかりとエラーハンドリングのできるコードを書くこと。
- コードを追いやすく、イメージもしやすいモジュール依存関係で書いていくこと。
- 適切な命名をすること。
です。パターンマッチやwith
自体を覚えることは決して難しくありませんが、これらのメンテナンス性に関わるような部分はちゃんと毎回意識して書かないと上達していかないなと思い、よくリファクタリングをしています。
おわりに
パイプライン演算子を濫用せずとも、Elixirは楽しく書いていくことができる言語です。したがって、面白くなくて読みやすいコードしか書いちゃいけなかったとしても、嫌にならずに学習を続けていくことができます。
Web開発をされている・興味があるみなさん、是非この楽しいElixirという言語に触れてみてほしいです。
拙い文章でしたが、読んでいただいてありがとうございました。