やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

elixirで2次元のリストを縦方向に結合(merge)する

日本語が下手な件

どういうことかというと...

#この配列を
_lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

#こうしたい
_output_image = [
  [1, "a", true],
  [2, "b", false],
  [3, "c", false]
]

要するに「2次元のリストを縦方向に結合(merge)する」ってことでしょ(強引
elixirでスクレイピングやった時に結構この手の操作があった
ある商品データの

  • 商品名
  • 金額
  • レビュータイトル
  • レビューテキスト

をdomの属性を使って抽出すると

scraping_res = [
  [4,5,5]
  ["goog", "いい品だ", "ふつくしい"]
  ["手軽に購入できてよかったです", "良いものだ", "強靭無敵最強!!"]
]

こんな感じの配列になって、これを以下のようにすると各商品の情報がまとまり
このままCSVデータとして書き込むことができる(あえてマップにしていない)

adj_res = [
  [4,  "good", "手軽に購入できてよかったです"]
  [5, "いい品だ", "良いものだ"]
  [5, "ふつくしい",  "強靭無敵最強!!"]
]

Enumを使う最も手軽な方法

一番楽なのはおそらくこれ

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

cal_res =
  lst_data 
    |> Enum.zip() 
    |> Enum.map(&(Tuple.to_list(&1)))

IO.inspect(cal_res)
#result
#[
#  [1, "a", true],
#  [2, "b", false],
#  [3, "c", false]
#]

もしくは

cal_res =
  lst_data 
    |> Stream.zip() 
    |> Stream.map(&(Tuple.to_list(&1)))

いいね

動き方

Enum.zip()の時点でほとんどの操作は終了している

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

cal_res =
  lst_data 
    |> Enum.zip() 

IO.inspect(cal_res)
#result
#[{1, "a", true}, {2, "b", false}, {3, "c", true}]

この時点で縦方向への結合は終了しているわけです(クソ便利
ただ、なぜかタプルに変換されてしまっているので元に戻してやろうねってことで
Enum.mapとTupleのto_listを使ってリストinリストに変換しています

zip_res = [{1, "a", true}, {2, "b", false}, {3, "c", true}]
cal_res =
  zip_res
    |> Enum.map(&(Tuple.to_list(&1)))

# Tuple.to_list({1, "a", true}) -> [1, "a", true]
# Tuple.to_list({2, "b", false}) -> [2, "b", false]
# Tuple.to_list({3, "c", true}) -> [3, "c", false]

手軽にやる方法は以上で終了です
あとは僕の自己満足です

一回タプルに変換されるのキモくない?

これが個人的には納得がいってない
なんでリストを一度、タプルに変換する必要があるのか...
コレガワカラナイ

タプルに変換しないモジュールを自分で作ってみました

とりえあず書いた

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]


defmodule Merge do
  def zip(lst_data) do
    exe_count = Enum.count(lst_data)
    _zip(lst_data, 0, exe_count, [])
  end
  defp _zip(_lst_data, _searcher, counter, accum) when counter === 0, do: accum
  defp _zip(lst_data, searcher, counter, accum) do
    select_values = zip_helper(lst_data, searcher)
    _zip(lst_data, searcher+1, counter-1, accum ++ [select_values])
  end


  def zip_helper(lst_data, searcher) do
    exe_count = List.first(lst_data) |> Enum.count()
    _zip_helper(lst_data, searcher, exe_count, [])
  end

  defp _zip_helper(_lst_data, _seracher, counter, accum) when counter === 0, do: accum
  defp _zip_helper([head | tail], seracher, counter, accum) do
    target = Enum.at(head, seracher)
    _zip_helper(tail, seracher, counter-1, accum++[target])
  end
end

res = Merge.zip(lst_data)
IO.puts("--> result")
IO.inspect(res)

#result: [[1, "a", true], [2, "b", false], [3, "c", true]]

全体の動きとしては再帰関数の中で再帰関数を呼んでいる
zip関数はリストを受け取り、要素の数だけzip_helper関数をcallする
zip_helper関数では受け取った配列とindex番号から縦方向に結合した配列を返す

#zip_helper
lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

zip_helper(lst_datam, 0)
# [1, _, _]
# ["a", _, _]
# [true, _, _]
#result: [1, "a", true]

pythonでいうfor in forのような動きをイメージして作りました

for column in data:
  for record in column:
    pass

csvデータを操作する時にこの手の処理はよくやる
ただEnum.reduceとか使えばもっと綺麗にかける気はする....
Enumにaccumulatorを渡す方法はないものなのか
もう少し研究します

追記:もっと簡単にかけるやん

アホみたいに再帰関数使ってたのを反省
Enum.mapで普通に取り出せることに気づきました
ただEnum.reduceかEnum.foldlあたりを使えばもっと短くできる気はする

elixir

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [true, false, true]
]

defmodule Merge do
  def easy_zip(lst_data) do
    _easy_zip(lst_data, 0, Enum.count(lst_data), [])
  end
  defp _easy_zip(_lstdata, _searcher, counter, accum) when counter === 0, do: accum
  defp _easy_zip(lst_data, searcher, counter, accum) do
    merge = Enum.map(lst_data, &(Enum.at(&1, searcher)))
    _easy_zip(lst_data, searcher+1, counter-1, accum ++ [merge])
  end
end

ついでにpython

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [True, False, True]
]

def merge_column(lst_data, index_num):
    merge = list(map(lambda x: x[index_num], lst_data))
    return merge
 
res = []
for i in range(len(lst_data)):
    merge = merge_column(lst_data, i)
    res.append(merge)
    
print(res)
#result: [[1, 'a', True], [2, 'b', False], [3, 'c', True]]

コメントより: pythonでクールに1行で

lst_data = [
  [1,2,3],
  ["a", "b", "c"],
  [True, False, True]
]

res = list(map(list, zip(*lst_data)))

うーん、zip(*lst_data)ってのは思いつきませんでした
zipから出力された結合されたタプルをmap関数使ってリストに戻して1行で終了
お見事です