作って学ぶEnumモジュール1、mapとto_list

[基礎の学習のロードマップ](https://qiita.com/pojiro/items/9f39db4d28aeaf591b23)の続編として
「作って学ぶEnumモジュール」を書いていきたいと思います。

前提知識は以下です。

- パターンマッチ
- レンジ
- モジュール、関数、無名関数

ではEnumモジュールの仕様を調べながら、MyEnumモジュール作ってみようと思います。

~~~elixir
iex(1)> h Enum. # ここでタブを入力するとEnumモジュールの関数が表示されます。
~~~

いろいろあるけど、最初はやっぱりmapかな。

# MyEnum.map

まず仕様を確認します。

~~~elixir
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]を返す

ということはガワはこんな感じ。

~~~my_enum.ex
defmodule MyEnum do
  def map(list, func) do
  end
end
~~~

このままでは何も返さないので、中身を考えていきます。
listの要素ひとつひとつ（1, 2, ...）にfunc（fn x -> x*2 end）を適用したいですが、
このままだとダメなのでパターンマッチをつかって要素を取り出せるようにします。

~~~my_enum.ex
defmodule MyEnum do
  def map([head|tail], func) do
    func.(head) # ついでに関数も適用しました。
  end
end
~~~

最終的にはリストを返したいから、こうかな。

~~~my_enum.ex
defmodule MyEnum do
  def map([head|tail], func) do
    [func.(head)]
  end
end
~~~

でも、これだと１つ目の要素にしかfuncを適用してないですね。残りのtailにも適用したいです。
tailはリストだったので、同じ関数が使えますね、map(tail, func)。ということは再帰になりそうです。
それと[func.(head)]とmap(tail, func)が返すリストの関係をつける必要があります。ちょっと考えてみてください。
。
。
。
。
。
。
。
。
たとえば、これでどうでしょうか。

~~~my_enum.ex
defmodule MyEnum do
  def map([head|tail], func) do
    [func.(head)|map(tail, func)]
  end
end
~~~

確認してみます。

~~~elixir
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>
~~~

エラーですね。「第一引数が[]のときにマッチする関数節が無いよ」的な感じですね。
再帰を書くときは終了条件が必要でした。

~~~my_enum.ex
defmodule MyEnum do
  def map([], func), do: [] # 終了条件を追加！空のリストには関数を適用する要素がないので[]を返す。
  def map([head|tail], func) do
    [func.(head)|map(tail, func)]
  end
end
~~~

もう一回実行しましよう。

~~~elixir
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]
~~~

お、ばっちり。じゃあ、もう少し試してみましょう。

~~~elixir
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

仕様は「レンジをリストに変える」です。ちょっと考えてみてください。
。
。
。
。
。
。
。
。

僕はこのように作ってみました。

~~~my_enum.ex
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は以下のようになりました。

~~~my_enum.ex
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」を予定しています。

**「いいね」よろしくお願いします！** :wink: