作って学ぶEnumモジュール2、reduceとsum, max, min, filterの続編として、
今回はEnumモジュールをオレオレ実装したので紹介します。
githubリポジトリ: oreore_implementation_of_myenum
※リポジトリ名は of_myenumではなく、of_enumとすべきでした
先に結論
この記事で何が言いたいかというと、
- 「Enumモジュールを自分で実装してみるのは良いコーディングドリルになるので、オススメ
」
ということです。
オレオレ実装について
Enumモジュールそのものを実装することを「オレオレ実装」と言っています。
すでにあるものをあえて自分で実装した理由は
以下を達成するのに良いコーディングドリルになるのではないかと考えたからです。
- Enumモジュールの理解を深める
- reduceの使い方の理解を深める
- Elixirのコーディング力の向上させる
実際に実装したことで以下が達成できました。
- Enumモジュールの関数群が持つ機能を知ることができた
- Elixirのコーディング力が向上した(はず!)
- reduceを繰り返し使うことで、使い方の勘所がつかめた
- mixを使ったTDD(Test-driven development)の雰囲気がつかめた
- テストがあることにより、修正への心理的閾値が低くなることがつかめた
- 知らなかった知見が実装することで得られた
なので、オススメです。
以下ではどのようにオレオレ実装を進めたかを紹介します。
進め方
以下のように進めました
- mixでプロジェクトを作成する
- mixで自動でテストが走るようにする
mixにはtest機能がありますが、自動で走らせられるようにして手返しをよくします - ひたすら実装する
では順に手順を紹介していきます。
mixでプロジェクトを作成する
作って学ぶmix、echoコマンドでmixの使い方の初歩を理解したので、mixを使って実装をしました。
$ mix new my_enum
$ cd my_enum
mixで自動でテストが走るようにする
実装は以下を繰り返すことになります。
- lib/my_enum.ex に関数を実装する
- test/my_enum_test.exs にテストを書く(本当のTDDはTestファースト)
- mix testでテストを実行する
- テストが通るまで上記を繰り返す
が、、すぐにmix testを繰り返し手で実行するのがめんどくさくなりました。
なんとか楽しようとぼやいていたら素晴らしいライブラリmix_test_watchを教えてもらいました。
ファイル更新を検知して、mix testを自動実行するライブラリです。
mix_test_watchの導入
mix.exsにライブラリに追記します。(プログラミングElixirだと、13.6節、p.138が参考になります。
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{ :mix_test_watch, "== 0.9.0", only: :dev, runtime: false}, # 追加
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
※僕が使った時の最新version1.0.1にはバグが存在したため、versionを0.9.0に固定しました。
ライブラリを取得します。
$ mix deps.get
そして、実行します。
$ mix test.watch
Running tests...
some
example
.......................................................................
Finished in 0.5 seconds
71 tests, 0 failures
Randomized with seed 789024
これだけでファイル更新を検知してテストを実行してくれるため、
実装すること、テストを書くことだけに集中できます。
以下は実装中の僕のデスクトップです。
画面左側で実装とテストを書き、右上でテスト結果の確認、右下で進捗率を修正したり、検索したりしていました。
ひたすら実装する
実装を進めると以下の事実にめげました。なのでちょっと工夫することにしました。
- 関数の多さ(例外を含めると全部で92個)
- どう組めばいいか分からない関数の存在
「関数の多さ」への対処
関数の多さにより、めげることを防ぐために進捗表を作りました。
D1が進捗率[%]です。進捗率をみて残り何%だ、頑張れと自分を励ましました。
また進捗率をtwitterでつぶやきました。「いいね」がもらえると励みになりました。
※これは案外書けるなって調子に乗ってたときです。結局プラス2日かかりました。
「どう組めばいいか分からない関数の存在」への対処
難しいものは後回しにしました(きっぱり)。
実装できるものから進めることで、以下のメリットがありました。
- 習熟が進む
- 作るリズムが出てくる
- 実装した関数が使えるようになる
オススメ といってますが、やっぱりドリル辛い時もありました。
実装が難しいものは、弱音を吐いたり、走ったり、ふて寝したりしつつも考え続けました。
僕は以下が辛かったです。
- sort_by、ソートのアルゴリズムを知らない
- zip、enumerableを縦に潰す系に慣れてない
- chunk_while、仕様が不鮮明、これはしょうがないか
辛いとはつまり、自分がどういうロジックを組むのに慣れていないか分かることでした。
大事なことですね。
得た知見
最後にこのドリルで得た知見を紹介します。
レンジはmap
実装の途中でenumerableにmapが含まれることに気づき、
to_listを以下のように修正しました。
def to_list(l) when is_list(l), do: l
def to_list(m) when is_map(m), do: Map.to_list(m) # 追記
def to_list(e..e), do: [e]
def to_list(s..e) when s < e, do: [s| to_list(s+1..e)]
def to_list(s..e) when s > e, do: [s| to_list(s-1..e)]
これにより、今まで通っていたテストが通らなくなりました。
調べるとレンジが追記したis_mapのパターンマッチにかかっていることは分かりましたが、
なぜか全くわかりません。そこで確認しました。
iex(1)> 1..5 |> is_map
true
衝撃でした。レンジは型だと思ってました。
(is_rangeがないのは不思議だなとも思ってましたが、気づきませんでした。
原因はレンジがmapだったからです
iex(2)> i 1..5
Term
1..5
Data type
Range
Description
# これは構造体、構造体は__struct__ keyを持つ !!!map!!!
This is a struct. Structs are maps with a __struct__ key.
Reference modules
Range, Map
Implemented protocols
Enumerable, IEx.Info, Inspect
修正そのものは簡単で、パターンマッチの優先度をレンジより下げればよいだけでした。
def to_list(l) when is_list(l), do: l
def to_list(e..e), do: [e]
def to_list(s..e) when s < e, do: [s| to_list(s+1..e)]
def to_list(s..e) when s > e, do: [s| to_list(s-1..e)]
def to_list(m) when is_map(m), do: Map.to_list(m) # レンジの下に移動
テストの効能
関数の実装を進めるとある関数がある関数の機能を包含していることに気づきました。
たとえば、max_byはmaxを包含します。
これが分かるとmaxの実装をmax_byで書き直そうとなるのですが、
このときにテストがある強みが発揮されます。
#def max(enumerable, empty_fallback \\ fn -> raise(Enum.EmptyError) end)
#def max([], empty_fallback), do: empty_fallback.()
# もともとのmax実装
#def max(enumerable, _) do
# enumerable
# |> reduce(
# fn
# x, acc when x > acc -> x
# _, acc -> acc
# end
# )
#end
# 書き直したmax
def max(enumerable, empty_fallback \\ fn -> raise(MyEnum.EmptyError) end) do
max_by(enumerable, fn x -> x end, empty_fallback)
end
def max_by(enumerable, fun, empty_fallback \\ fn -> raise(MyEnum.EmptyError) end) do
cond do
enumerable |> empty? -> empty_fallback.()
true ->
enumerable
|> reduce(
fn x, acc ->
cond do
fun.(x) > fun.(acc) -> x
true -> acc
end
end
)
end
end
max実装時にテストを書いているので、
max_byに書き直したときに仮にバグが混入しても気づくことができます。
影響範囲を考えることを頭のそとに追い出せるのはとーっても嬉しいです。
最後に感想
辛いけどためになってオススメ です。
これぐらいを辛くもなんともないわと思えるまで頑張ります
githubリポジトリ: oreore_implementation_of_myenum もよかったら見てね。
次回は清流Elixirで学習したマクロをまとめようと思います。
※2019/11/18 プログラミングElixirの20章を精読したのですが、
「分かった!」となるには時間がかかりそうで、まとめるのはすぐには無理そうです。
「いいね」よろしくお願いします。