基礎の学習のロードマップの続編として
「作って学ぶEnumモジュール」を書いていきたいと思います。
前提知識は以下です。
- パターンマッチ
- レンジ
- モジュール、関数、無名関数
ではEnumモジュールの仕様を調べながら、MyEnumモジュール作ってみようと思います。
iex(1)> h Enum. # ここでタブを入力するとEnumモジュールの関数が表示されます。
いろいろあるけど、最初はやっぱりmapかな。
MyEnum.map
まず仕様を確認します。
iex(1)> h Enum.map/2 # 関数の後ろに続く「/2」は引数の数を表します。
def map(enumerable, fun)
@spec map(t(), (element() -> any())) :: list()
Returns a list where each element is the result of invoking fun on each
corresponding element of enumerable.
For maps, the function expects a key-value tuple.
## Examples
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map([a: 1, b: 2], fn {k, v} -> {k, -v} end)
[a: -1, b: -2]
ざっくり読むと以下でしょうか。
- def map(enumerable, fun)となっているので、第一引数は数え上げできるもの、第二引数は関数
- enumerableに[1,2,3], funにfn x -> x * 2 endが与えられたら[2,4,6]を返す
ということはガワはこんな感じ。
defmodule MyEnum do
def map(list, func) do
end
end
このままでは何も返さないので、中身を考えていきます。
listの要素ひとつひとつ(1, 2, ...)にfunc(fn x -> x*2 end)を適用したいですが、
このままだとダメなのでパターンマッチをつかって要素を取り出せるようにします。
defmodule MyEnum do
def map([head|tail], func) do
func.(head) # ついでに関数も適用しました。
end
end
最終的にはリストを返したいから、こうかな。
defmodule MyEnum do
def map([head|tail], func) do
[func.(head)]
end
end
でも、これだと1つ目の要素にしかfuncを適用してないですね。残りのtailにも適用したいです。
tailはリストだったので、同じ関数が使えますね、map(tail, func)。ということは再帰になりそうです。
それと[func.(head)]とmap(tail, func)が返すリストの関係をつける必要があります。ちょっと考えてみてください。
。
。
。
。
。
。
。
。
たとえば、これでどうでしょうか。
defmodule MyEnum do
def map([head|tail], func) do
[func.(head)|map(tail, func)]
end
end
確認してみます。
iex(1)> c "my_enum.ex" # モジュールファイルをコンパイル
[MyEnum] # 読み込まれました。
iex(2)> MyEnum.map([1,2,3], fn x -> x*2 end) # ドットまででタブを押すと保管されます。
** (FunctionClauseError) no function clause matching in MyEnum.map/2
The following arguments were given to MyEnum.map/2:
# 1
[]
# 2
#Function<7.126501267/1 in :erl_eval.expr/5>
エラーですね。「第一引数が[]のときにマッチする関数節が無いよ」的な感じですね。
再帰を書くときは終了条件が必要でした。
defmodule MyEnum do
def map([], func), do: [] # 終了条件を追加!空のリストには関数を適用する要素がないので[]を返す。
def map([head|tail], func) do
[func.(head)|map(tail, func)]
end
end
もう一回実行しましよう。
iex(3)> r MyEnum # モジュール修正したのでリコンパイル
# 再定義されたよというwarning、リコンパイルしたからですね。無視してよさそう
warning: redefining module MyEnum (current version defined in memory)
my_enum2.ex:1
# 使ってない変数があるよというwarning、「使うつもりがないならアンダースコアの接頭辞をつけて」らしいです。
# あとでつけましょう。
warning: variable "func" is unused (if the variable is not meant to be used, prefix it with an underscore)
my_enum2.ex:2: MyEnum.map/2
{:reloaded, MyEnum, [MyEnum]}
iex(4)> MyEnum.map([1,2,3], fn x -> x*2 end)
[2, 4, 6]
お、ばっちり。じゃあ、もう少し試してみましょう。
iex(5)> MyEnum.map(1..10, fn x -> x*2 end)
** (FunctionClauseError) no function clause matching in MyEnum.map/2
The following arguments were given to MyEnum.map/2:
# 1
1..10
# 2
#Function<7.126501267/1 in :erl_eval.expr/5>
my_enum2.ex:2: MyEnum.map/2
またエラーです。関数がないよと。なぜかわかりますか?
MyEnum.mapは第一引数にリストは受け付けますが、レンジが受け付けられません。
レンジをリストに変換できたら、解決しそうです。
Enumにはto_list/1がありました。次はそれを作ってみましょう。
MyEnum.to_list
仕様は「レンジをリストに変える」です。ちょっと考えてみてください。
。
。
。
。
。
。
。
。
僕はこのように作ってみました。
defmodule MyEnum do
def map([], _func), do: []
def map([h|t], func), do: [func.(h)|map(t, func)] # 一行にまとめました。
def to_list(e..e), do: [e] # 再帰の終了条件, when s == 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)] # こっちは思いつきましたか?
end
to_listができると第一引数にレンジをうけるmapが作れるので、最終的なMyEnumは以下のようになりました。
defmodule MyEnum do
def map([], _func), do: []
def map([h|t], func), do: [func.(h)|map(t, func)]
def map(s..e, func), do: map(to_list(s..e), func)
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 reverse(s..e), do: to_list(e..s) # おまけ
end
今回はここまでです。
まとめ
- Enumモジュールのmap関数を仕様を調べて作ってみました。
- 作る上ではリストとレンジのパターンマッチ、再帰が有効なツールというのが分かりました。(分かりました?)
次回は「作って学ぶEnumモジュール2、reduce」を予定しています。
「いいね」よろしくお願いします!