Elixir
ElixirDay 15

リストの要素からインデックス位置を返す

More than 3 years have passed since last update.

Elixir Advent Calendar 2014 15日目。
今日もライトなネタを。リストの要素からインデックス位置を返したいと思います。

やりたいこと

例えば、こういうリストがあるとします。

  list = [1, 2, 3, 2, 1]

リストとリストの要素パラメタを渡すと、インデックス位置のリストを返します。
例えば、上記リストと1を渡すと、[0, 4] を返します。
与えられたパラメタがリストに存在しない場合、空のリスト [] を返します。

ちなみに、余談ですが、Rubyの場合はこんな感じで実装します。

class Array
  def get_index(param)
    self
      .each_with_index
      .select{|x| x[0] == param}
      .map{|x| x[1]}
  end
end

動機

ElixirのEnumモジュール及びListモジュールには、インデックス位置を引数に取る関数があります。

  • Enum.at/3
  • Enum.fetch/2
  • Enum.fetch!/2
  • List.delete_at/2
  • List.insert_at/3
  • List.replace_at/3
  • List.update_at/3

ここで誰もが思うわけです。
「インデックス位置って、どうやって知るんだっけ?」

標準関数を眺めてみる

とりあえず、標準関数の処理系を眺めてみます。
List.keyfind/4Enum.find/3Enum.find_index/2Enum.with_index/1 あたりを見ればよいでしょうか?(適当)

List.keyfind/4

@spec keyfind([tuple], any, non_neg_integer, any) :: any
def keyfind(list, key, position, default \\ nil) do
  :lists.keyfind(key, position + 1, list) || default
end

これはイメージと違いますね。Tupled List専用です。

Enum.find/3

@spec find(t, default, (element -> any)) :: element | default
def find(collection, ifnone \\ nil, fun)

def find(collection, ifnone, fun) when is_list(collection) do
  do_find(collection, ifnone, fun)
end

def find(collection, ifnone, fun) do
  Enumerable.reduce(collection, {:cont, ifnone}, fn(entry, ifnone) ->
    if fun.(entry), do: {:halt, entry}, else: {:cont, ifnone}
  end) |> elem(1)
end

defp do_find([h|t], ifnone, fun) do
  if fun.(h) do
    h
  else
    do_find(t, ifnone, fun)
  end
end

defp do_find([], ifnone, _) do
  ifnone
end

Elixirのこういうソースコードはとても美しいと思います。
関数作って第三引数に渡すなりすれば良さそうだと、ぱっと見は思うのですが、それは間違いです。
この関数は、funにマッチした最初の要素の値を返します。

Enum.find_index/2

@spec find_index(t, (element -> any)) :: index | :nil
def find_index(collection, fun) when is_list(collection) do
  do_find_index(collection, 0, fun)
end

defp do_find_index([h|t], counter, fun) do
  if fun.(h) do
    counter
  else
    do_find_index(t, counter + 1, fun)
  end
end

defp do_find_index([], _, _) do
  nil
end

関数作って第二引数に渡すなりすれば良さそうだと、ぱっと見は思うのですが、それも間違いです。
この関数は、funにマッチした最初の要素のインデックス位置しか返しません。
(Rubyで言うとArray#indexに相当します。)

Enum.with_index/1

@spec with_index(t) :: list({element, non_neg_integer})
def with_index(collection) do
  map_reduce(collection, 0, fn x, acc ->
    {{x, acc}, acc + 1}
  end) |> elem(0)
end

イメージに一番近いのはこれですね。ただ、今回はインデックスだけ返してくれれば良いので、少し冗長です。

作ってみる

無い物は作るのみ。こんな感じでいかがでしょう。

defmodule EnumX do

  def get_index(collection, param) when is_list(collection) do
    Enum.with_index(collection)
    |> Enum.filter_map(fn {x, _} -> x == param end, fn {_, i} -> i end)
  end

end

実行してみます。

list = [1, 2, 3, 2, 1]

iex> EnumX.get_index(list, 0)
[]
iex> EnumX.get_index(list, 1)
[0, 4]
iex> EnumX.get_index(list, 2)
[1, 3]
iex> EnumX.get_index(list, 3)
[2]
iex> EnumX.get_index(list, 4)
[]

意図した通りに動作します。

まとめ

リストの要素からインデックス位置を返す関数が無かったので、作ってみました。
これでElixirの活用の幅も広がりますね。

明日は @k1complete さんです。