LoginSignup
7
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-12-15

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 さんです。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7