LoginSignup
6
1

More than 3 years have passed since last update.

【老害が高級言語で苦しんでますシリーズ2】ElixirのEnum.chunk_whileが何も分からん

Last updated at Posted at 2020-02-24

プロフを見ても分るかと思いますが、私は老害なもので今風のプログラミングがとてもとても苦手です。
前回もこんな事で苦労しているぐらいですし(汗

今回はElixirのEnum.chunk_whileがテーマです。

Enum.chunk_whileについて

公式の説明はここ。これでガンガンコードが組めるとか今どきの若い人は凄いですよねー。

Examples見ても中で何やっているのかがサッパリわかりま千円(おやじギャグですよー)。

contって何?、accとかどっから沸いて来た?、elementはどうも与えたリストの各値なのだろうと。で戻り値が複数あるとか、after_funのパターンマッチっぽいの何これ?、後なんでreverseしてるの。特にafter_funの方…

で、色々調べるとこういう事なようです(自信はありません)

  • chunk_whileでは与えられたリストの各値毎に指定された関数を呼び出して処理させる事が出来るみたいです。
  • リストを引数指定するやり方だと以下のように使えるみたいです。

    
    Enum.chunk_while(enumerable, # :処理させたいリスト 
        acc, # ACCの初期値。ACCはこのEnum内で共通に使える変数。その初期値を入れます, 
        chunk_fun, #:各値毎に呼び出す関数, 
        after_fun)  # :処理完了時に呼び出す後始末用の関数
    
    # chunk_funの引数は、enumerable内の各値と処理間で使えるaccの2つ
    # chunk_funの戻り値は、chunkに加えたいデータがあれば  
    #                        {:cont, 出力に加えたいリスト, accの値}
    #            chunkに加えたいデータが無ければ
    #                          {:cont, accの値}               
    # :contは恐らく制御系の記述で、ドキュメントにもあるように、
    # contと書いて継続させるように書けば良いっぽい?。
    
    # after_funの引数は、accのみ。
    # after_funの戻り値はchunk_funと同じです。accの値は多分使われないので
    # []とかにすれば良いのかな(chunk_funと型を合わせている?)
    
  • なのでenumerableの各値に対してchunk_fun(element=値, acc)を呼び出すみたいです。

  • chunk_funの戻り値はaccと、後リストも任意で追加出来ます。accはその値に更新します。

  • リストがくれば戻り値に追加します。

  • で更新したaccと次のenumerable値でchunk_funを呼び出して・・の繰り返しです。

  • 全部終わったらafter_funを呼び出します。これは恐らく処理が途中で終わった場合に、それに始末をつけるためにあるのではないかと。この戻り値にリストがあればそれも戻り値に加えます。

  • なので、chunk_whileの戻り値は基本、リストのリストとなります。

うん、やっぱり分かりませんねごめんなさい。実際に動かしてみようと思います。

動かして確認してみる

筆者の環境

本記事の内容を実行しながら確認したい場合は、Elixirを入れてiexが動くようにして下さい。私の環境は以下です。

  • Ubuntu1804(64bit)
  • Erlang 10.6.2
  • Elixir 1.10.0

インストールは公式だとここです。ここらはググると割合色々出てくる所になるかと思います。

プロジェクト作成

以下の感じでプロジェクトを作成して、iexを起動させます。

# mix new enum_test
# cd enum_test
# iex -S enum_test

まずは何もしない。

で、libフォルダにあるenum_test.exを次のように書きます。

defmodule EnumTest do

  def chunk_while_test do

    chunk_fun = fn element, acc ->
      IO.puts "--------->chunk_fun element,acc"
      IO.inspect element
      IO.inspect acc
      {:cont, []}
    end

    after_fun = fn acc ->
        IO.puts "--------->after_fun acc"
        IO.inspect acc
        {:cont, []}
    end

    src = Enum.to_list 1001..1005
    IO.puts "==========>SRC"
    IO.inspect src
    result = Enum.chunk_while(src, [], chunk_fun, after_fun)
    IO.puts "==========>RESULT"
    IO.inspect result

    :ok

  end

end

処理内容は

  • [1001, 1002, 1003, 1004, 1005]のリストでchunk_whileします。
  • 入力状態を表示し、 {:cont, []}つまりリストも無ければ、accも空のままで処理させます。

修正してiexを起動、recompile してEnumTest.chunk_while_test して実行させてみますと、

==========>SRC
[1001, 1002, 1003, 1004, 1005]
--------->chunk_fun element,acc
1001
[]
--------->chunk_fun element,acc
1002
[]
--------->chunk_fun element,acc
1003
[]
--------->chunk_fun element,acc
1004
[]
--------->chunk_fun element,acc
1005
[]
--------->after_fun acc
[]
==========>RESULT
[]
:ok

となります。確かにリストの各値が入ってきますね。今はそれだけで何もしないので、出力も空のリストが出てくるだけです。

大事な事なので2回言いましょう

次に出力側を加工してみます。全ての結果を[入力値, 入力値]とします。

  def chunk_while_test do

    chunk_fun = fn element, acc ->
      IO.puts "--------->chunk_fun element,acc"
      IO.inspect element
      IO.inspect acc
      {:cont, [element, element], []} #大事な事なので2回言います
    end

    # 以下は同じです。

  end

これを実行すると

==========>SRC
[1001, 1002, 1003, 1004, 1005]
--------->chunk_fun element,acc
1001
[]
--------->chunk_fun element,acc
1002
[]
--------->chunk_fun element,acc
1003
[]
--------->chunk_fun element,acc
1004
[]
--------->chunk_fun element,acc
1005
[]
--------->after_fun acc
[]
==========>RESULT
[[1001, 1001], [1002, 1002], [1003, 1003], [1004, 1004], [1005, 1005]]
:ok

となります。RESULTを見ると分かりますが、記述通りで各値が2つ入った物が来ています。公式ドキュメントには「リストのリストが戻る」とありましたが、まさにこの通りです。

足し算させてみる

次は足し算をさせてみましょう。以下の感じです。


  def chunk_while_test do

    chunk_fun = fn element, acc ->
      IO.puts "--------->chunk_fun element,acc"
      IO.inspect element
      IO.inspect acc
      cond do
        acc == [] -> # accが空(初期状態)
          {:cont, [element], [element]}  # 最初は値をどちらもそのまま
        true ->
          before_val = Enum.at(acc, 0)
          after_val = before_val + element
          {:cont, [before_val, element, after_val], [after_val]}
            # [今までの合計値、今の値、新しい合計値]を追加させます
            # accには新しい合計値を入れます。
      end
    end

    # 以下は同じです。

  end
  • chunk_funが少し複雑になっています。
  • 条件によって処理を振り分けています。最初は acc=[] で来ますので、初期値(element=この場合1001)を設定します。
  • 次からはaccに値が入ってますので、これを取り出して、今の値(element)を足します。
    • 出力されるリスト(chunk)は 前までの合計値、今の値、新しい合計値 を入れます
    • accには 新しい合計値 のみのリストを入れます。

これを実行すると

==========>SRC
[1001, 1002, 1003, 1004, 1005]
--------->chunk_fun element,acc
1001
[]
--------->chunk_fun element,acc
1002
[1001]
--------->chunk_fun element,acc
1003
[2003]
--------->chunk_fun element,acc
1004
[3006]
--------->chunk_fun element,acc
1005
[4010]
--------->after_fun acc
[5015]
==========>RESULT
[
  [1001],
  [1001, 1002, 2003],
  [2003, 1003, 3006],
  [3006, 1004, 4010],
  [4010, 1005, 5015]
]
:ok

こうなります。RESULTを見ると分かりますが、足し算の経緯が出てきています。

結果を加工してみる

次にafter_funをいじってみましょう。足し算の結果を二倍した値を最後に追加させます。after_funを今度は修正します。


  def chunk_while_test do

    # chunk_funは同じです。

    after_fun = fn acc ->
        IO.puts "--------->after_fun acc"
        IO.inspect acc
        result = Enum.at(acc, 0) * 2
        {:cont, [result], []}
          # 合計値の2倍をリストに追加します
          #(なので引数が3つになってますよ注意)
    end

    # 以下は同じです。

戻り値の数を3つにして、合計値の二倍を追加していますので結果にこれが反映される筈です。これを実行すると

==========>SRC
[1001, 1002, 1003, 1004, 1005]
--------->chunk_fun element,acc
1001
[]
--------->chunk_fun element,acc
1002
[1001]
--------->chunk_fun element,acc
1003
[2003]
--------->chunk_fun element,acc
1004
[3006]
--------->chunk_fun element,acc
1005
[4010]
--------->after_fun acc
[5015]
==========>RESULT
[
  [1001],
  [1001, 1002, 2003],
  [2003, 1003, 3006],
  [3006, 1004, 4010],
  [4010, 1005, 5015],
  [10030]
]
:ok

目論見通り、最後に合計値の2倍の値が付与されています。

さぁ、これで公式サンプルが読めるのではないだろうか

と遊んで見た所で公式サンプルを見てみます。


chunk_fun = fn element, acc ->
  if rem(element, 2) == 0 do
    {:cont, Enum.reverse([element | acc]), []}
  else
    {:cont, [element | acc]}
  end
end
after_fun = fn
  [] -> {:cont, []}
  acc -> {:cont, Enum.reverse(acc), []}
end

iex> Enum.chunk_while(1..10, [], chunk_fun, after_fun)
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]

# https://hexdocs.pm/elixir/Enum.html#chunk_while/4 から。

  • chunk_funを見ると2で割り切れるかどうかで処理が分かれます
  • 最初はelementが1なので、まずは下側の2で割り切れない側(elseサイド)を見ます。この場合はaccのみ更新でelementをaccに加えています。で、最初はaccは[]でelementは1なので[1]になるです。
  • 次はelementが2なので、rem(element, 2) == 0側に来ます。この場合にelementをaccに加えています。|でつなぐと[2,1]です。これをEnum.reverseで逆にしたリスト[1,2]を結果に追加しています。でaccは空リストにしてます。
  • 次はelementが3なので、2で割り切れない側(elseサイド)に来ます。この場合はaccのみ更新で上と同じ理屈で[3]となります。
  • という感じでchunk_funが繰り返されます。
  • 最後のelementは10なので、chunk_funで[9,10]が作られ。accが空になった状態でafter_funに来ますので、特に結果に影響は出ない感じです。

ちなみにchunk_while呼び出しているトコで1..10とある部分を1..11にするとこうなります。

[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]

11が追加されます。これはafter_funのaccが空ではないからです。個人的にはafter_funでreverseする必要性は良く分からないです(汗。

おまけ:連番検出プログラム

で、そもそもなんでchunk_whileに興味を持ったのかといえば、こういう事がしたかったからなのでした。


# お題;連番を検出したい

# 例えば以下のようにリストがあったとします。

[1001, 1004, 1005, 1008, 1010, 1011, 1012, 1015, 1016]

# この内、連番となっている物を抜き出したいのです。
# 上記なら1004,1005と1010,1011,1012。それに 1015,1016 です。
# 事前に sortは終わってるものとします。
# つまり↓の感じに。

[1004, 1005], [1010, 1011, 1012], [1015, 1016]]

で、ElixirのEnumの説明を眺めているとchunk_whileがハマりそうで、例を見てなんじゃこりゃとなったです。上記の感じで動きを把握して、以下のように組んでみました(これまだバグとかあるかも。。)

defmodule EnumTest do

  def chunk_while_test do

    chunk_fun = fn element, acc ->
      latest = Enum.at(acc, 0)  # 「|」連結なので、latestは常にトップ(最初はnil)
      cond do
        latest == nil ->
          # latestが無いなら最初のデータを意味します。accはelementを。
          {:cont, [element]}
        element - latest == 1 ->
          # 前のデータとの差分が1なので絶賛連番継続中
          {:cont, [element | acc]} # accにelementを追加(連番蓄積)
        true ->
          # latestが存差して、差分が1じゃないなら、ここで連番を打ち切ります
          cond do
            # accに連番が積んであるので、それでchunkに。accはelement(latest)
            Enum.count(acc) == 1 ->  {:cont, [element]} # 1個は連番とは言わんよねー
            true ->  {:cont, Enum.reverse(acc), [element]}
          end
      end
    end

    after_fun = fn acc ->
      cond do
        # accに連番が積んであるので、それでchunkに。accはelement(latest)
        Enum.count(acc) <= 1 ->  {:cont, []} # 1個は連番とは言わんよねー
        true ->  {:cont, Enum.reverse(acc), []}
      end
    end

    src = [1001, 1004, 1005, 1008, 1010, 1011, 1012, 1015]
    IO.puts "==========>SRC"
    IO.inspect src
    result = Enum.chunk_while(src, [], chunk_fun, after_fun)
    IO.puts "==========>RESULT"
    IO.inspect result

    :ok

  end

end
  • 前回との差分を見て1ならば連番という考え方が基本
  • chunk_funは以下3つのパート
    • 最初のデータ。accが空な状態ならまずはelementのみをaccに設定。戻り値はaccのみ
    • 連番途中。(elementの値) ー (最後にaccに入れたデータ) = 1なら連番状態、elementをaccに追加します。戻り値はaccのみ
    • 連番終了。上記の条件じゃなければ連番が切れたとして処理します。戻り値は空にしたaccと今までためてた連番のリスト(ただし2個以上あること)
  • after_funでは、accを見て2個以上あれば、連番途中で処理が終了したとして、それも戻り値に加えています。

で、実行するとこうでした。

==========>SRC
[1001, 1004, 1005, 1008, 1010, 1011, 1012, 1015, 1016]
==========>RESULT
[[1004, 1005], [1010, 1011, 1012], [1015, 1016]]
:ok

一応動いているような気がします…

以上です。やっぱ私回帰系は(も?)ダメなんだなーと実感ですね(汗

6
1
2

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
6
1