Help us understand the problem. What is going on with this article?

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

基礎の学習のロードマップの続編として
「作って学ぶ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]を返す

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

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

でも、これだと1つ目の要素にしか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

確認してみます。

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

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

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

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







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

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:

fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ
https://fukuokaex.fun/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした