LoginSignup
1

posted at

updated at

Organization

Enum.max_by/4 を説明します(Elixir)

みかの原わきて流るる泉川いつ見きとてか恋しかるらむ

Advent Calendar 2022 77日目1の記事です。
I'm looking forward to 12/25,2022 :santa::santa_tone1::santa_tone2::santa_tone3::santa_tone4::santa_tone5:
私のAdvent Calendar 2022 一覧


はじめに

Elixirを楽しんでいますか:bangbang::bangbang::bangbang:

@t-yamanashi さんのツイートをみました。
Enum.max_by/4の4番目の引数って何だったっけ? と私はおもいだせませんでした。
そこで調べてみました。

結論だけいうと、4番目の引数は、empty_fallbackです。
つまり、enumerableが空のときにどうするの? を決定する関数を指定します。
ただ案外奥が深いのです。
(涙なしには語れません。聞くも涙語るも涙の物語です)
興味深いことがいろいろとありますので、記事にしたためました。

モチベーション

@t-yamanashi さんの謙遜があるかもしれませんし、すでに解決されていることだろうとおもいます。
@piacerex ピアちゃんが適切なアドバイスをTwitter上でされています。

Twitterのやりとりそのものが有益だとおもいました。
また、Enum.max_by/4の私の理解の過程を記すことは将来、同じことをおもうであろう方の助けになるとおもい、Qiitaに記すことにしました。

私は、Advent Calendar 2022を完成させたく、ネタを探しています。

Enum.max_by/4

まずは公式ドキュメントを眺めてみます。

スクリーンショット 2022-03-20 7.46.16.png

第一引数のenumerableなものから最大値を返す関数です。
要素内の何をもって比較するのかは第二引数のfunで指定します。
第一引数と第二引数は必ず指定する必要があります。

第三引数と第四引数はデフォルトが設定されていて、指定は必須ではありません。
第三引数は、話が長くなるので後述します。
第四引数は、enumerableが空のときに何とする? を指定することができます。

Term ordering

Elixirは、異なる型どうしの大小比較ができます。
そのルールは、Term orderingに書いてあります。

number < atom < reference < function < port < pid < tuple < map < list < bitstring

例です。
整数とアトムを比較してみます。

iex> 1 < :an_atom
true

これはそういうものだとおもってください。
さきほど端折った第三引数の話は、mapの比較と関係があります。
ここでもまた飛ばします。

異なる型どうしの大小比較は以降、この記事では取り扱いませんが、Elixirの大小比較の特徴ですのでご紹介しておきました。

基本的な使い方 ーー 第一引数と第二引数を指定

整数同士を比較してみます。

iex> Enum.max_by([1, 3, 100, -1], fn n -> n end)
100

iex> Enum.max_by([1, 3, 100, -1], & &1)
100

文字列同士を比較してみます。

iex> Enum.max_by(["osamu", "awesome"], fn s -> s end)
"osamu"

iex> Enum.max_by(["osamu", "awesome"], & &1)
"osamu"

文字列の長さで比較してみます。

iex> Enum.max_by(["osamu", "awesome"], fn s -> String.length(s) end)
"awesome"

iex> Enum.max_by(["osamu", "awesome"], &String.length/1)
"awesome"

mapを比較してみます。

iex> list = [
  %{level: 60, name: "smilesmilesmile"},
  %{level: 99, name: "awesome"},
  %{level: 98, name: "torifuku"}
]
iex> Enum.max_by(list, fn map -> map end)
%{level: 99, name: "awesome"}

iex> Enum.max_by(list, & &1)
%{level: 99, name: "awesome"}

iex> Enum.max_by(list, fn %{name: name} -> name end)
%{level: 98, name: "torifuku"}

iex> Enum.max_by(list, fn %{name: name} -> String.length(name) end)
%{level: 60, name: "smilesmilesmile"}

結果から推測すると、ここでのmap同士の比較はまず:levelキーの値で行われているようです。

このように、第二引数の関数で何をもって比較するのかを指定します。

第三引数 ーー sorter

想像してください。

ある日、世紀の大発見がされました。
一夜にしてそれまでの概念がひっくり返るのです。
負の数のほうが大きいと決まったのです。

iex> Enum.max_by([1, 3, 100, -1], fn n -> n end, &</2)
-1

これはわかるようでわからない話だとおもうのでこのへんでやめておきます。

また別の話として、&</2があんまりにもカッコ良すぎるのでかえってわかりにくいかもしれません。
関数キャプチャではなく無名関数で書くと以下のようになります。

iex> Enum.max_by([1, 3, 100, -1], fn n -> n end, fn a, b -> a < b end)
-1

この第三引数のややこしいところは2種類の指定方法があることです。

  • true or false を返す関数
  • compare(a, b)関数(戻り値は:lt:eq:gt)を持つモジュール

Enum.max_by/3 が実はある

さらにややこしい話をします。
第3引数を省略して第4引数のみを指定できることです。

Enum.max_by([], &String.length/1, fn -> nil end)

Enum.max_by([], &String.length/1, &>=/2, fn -> nil end)

は同じなのです。

Enum.max_by([], &String.length/1, fn -> nil end)

上記のfn -> nil endは、Enum.max_by/4の第三引数かと言うとNoです。
Enum.max_by/4においては、第四引数です。

ふつうは、前から順に埋められていきます。
なぜ、fn -> nil endが、Enum.max_by/4の第四引数になり得るかというと、Enum.max_by/3に秘密があります。

Enum.max_by/3というドキュメントにはないメソッドがあるのです。
その実装をみると以下のようになっています。

  @doc false
  @spec max_by(
          t,
          (element -> any),
          (() -> empty_result) | (element, element -> boolean) | module()
        ) :: element | empty_result
        when empty_result: any
  def max_by(enumerable, fun, empty_fallback)
      when is_function(fun, 1) and is_function(empty_fallback, 0) do
    max_by(enumerable, fun, &>=/2, empty_fallback)
  end

ね!
これにより、

Enum.max_by([], &String.length/1, fn -> nil end)

Enum.max_by([], &String.length/1, &>=/2, fn -> nil end)

は同じなわけです。
これはけっこう特殊なケースだとおもわれます。

なぜこんなことをしているのかについては、Elixir 1.9.4の後方互換性を保つためです。
Elixir 1.9.4では、「Enum.max_by/3」がドキュメントに書いてありまして、この場合の第三引数は、empty_fallback \\ fn -> raise(Enum.EmptyError) endです。

Prior to this PR, there was no convenient way of getting the earliest/latest calendar type nor get the min/max datatype driven by a calendar type.

calendar typeの大小比較を容易にするために、Enum.max_by/4の第三引数でsorterが追加されたとのことです。

構造体の比較はsorterを指定する

もっぱら、構造体の比較で使います。
使う必要があります。

Enum.max_by/4で示されている例をもとに説明を続けます。

iex> users = [
  %{name: "Ellis", birthday: ~D[1943-05-11]},
  %{name: "Lovelace", birthday: ~D[1815-12-10]},
  %{name: "Turing", birthday: ~D[1912-06-23]},
  %{name: "Awesome", birthday: ~D[1979-01-01]}
]
iex> Enum.max_by(users, &(&1.birthday))
%{birthday: ~D[1912-06-23], name: "Turing"}

誕生日で比較しています。
これはあなたが望んだ通りの結果でしょうか。
もちろんYesという場合もあるでしょう。
ですが、たいていはNoでしょう。
望む結果は%{name: "Awesome", birthday: ~D[1979-01-01]}ではないでしょうか。

Structural comparison」に説明が書いてあります。
そこからポイントを抜き出すと、Date構造体そのもの同士の大小比較では、:day:month:yearの順に比較をすることになるとのことです。
では、私達が望む日付同士の比較をするにはどうしたらよいのでしょうか。
そうです。sorterです。

答えを書きます。

iex> users = [
  %{name: "Ellis", birthday: ~D[1943-05-11]},
  %{name: "Lovelace", birthday: ~D[1815-12-10]},
  %{name: "Turing", birthday: ~D[1912-06-23]},
  %{name: "Awesome", birthday: ~D[1979-01-01]}
]
iex> Enum.max_by(users, &(&1.birthday), Date)
%{birthday: ~D[1979-01-01], name: "Awesome"}

compare/2関数を持つモジュールを指定します。
compare/2関数は、:lt (less-than), :eq (equal to), and :gt (greater-than)を返す関数です。

以下、単なる実験です。
今回はやる意味はほとんどありません。
以下のようにcompare/2関数を持つHogeモジュールをつくれば指定できます。

iex> defmodule Hoge do
  def compare(d1, d2), do: Date.compare(d1, d2)
end

iex> Enum.max_by(users, &(&1.birthday), Hoge)
%{birthday: ~D[1979-01-01], name: "Awesome"}

独自の構造体を大小比較したいときに使う場面があります。

第四引数 ーー empty_fallback

いよいよラストの引数です。
empty_fallbackです。

iex> Enum.max_by([], &String.length/1, &>=/2)
** (Enum.EmptyError) empty error
    (elixir 1.13.1) lib/enum.ex:1860: anonymous fn/0 in Enum.max_by/2

上記の例の通り、第一引数のenumerableが空である場合には比較のしようがないのでデフォルトでは例外が発生します。
そこで、empty_fallbackを指定する出番です。

iex> Enum.max_by([], &String.length/1, &>=/2, fn -> nil end)
nil

例外が発生せず、nilが返せました :tada::tada::tada:

一体、これの何がうれしいのでしょうか? とおもった方もいらっしゃるかもしれません。
たとえば以下のように、第一引数のenumerableが空ではない場合には、第4引数はなにも働きません。

iex> Enum.max_by(1..100, & &1, &>=/2, fn -> nil end)            
100

それでいいのです。
このコードの断片だけみていても想像はしにくいとおもいますが、第一引数はファイルから読み取った値であるとか、ユーザが入力した値だとおもってください。

もう、おわかりですよね!
そうです。
十分、空だという場合はありえますよね。
その場合に、例外で落としてよければそのままでよいし、未入力として処理を続行したい場合にはnilなり、0なりを結果にして処理を続行すればよいわけです。

ちなみに公式に書いてある

Enum.max_by([], &String.length/1, fn -> nil end)

の例は
第1引数、第2引数、第4引数が指定されています。
第3引数はデフォルトの&>=/2が使用されます。
ということを記事を書きながら改めて知りました。
なぜこんなことができるのかについてはこの記事の上のほうで説明しました。

これで、Enum.max_by/4の説明を終わります。

付録

以下、付録です。

絵付きの解説記事に出会えるかもしれない方法

絵付きの解説記事を@piacerex ピアちゃんが紹介されていました。

@piacerex ピアちゃん
「リファレンスが分かりにくいときは、
関数名+アリティまんまでググりましょう
これは、すんごいテクニックです!!!

ググり結果の良記事は以下です。

Enumモジュールと仲良くなりたい!

わかります。
Enumモジュールと仲良くなるとElixirはますます楽しくなります。

@cooldaemon さんの記事です。

全編オススメです。
この記事と特に関連するのは195ページあたりからです。
さらに1ページだけ絞り込むと、272ページです。

スクリーンショット 2022-03-20 9.15.09.png

ここに上がっている関数を理解できれていればたいていのことは事足りるようにおもいます。

続・Enumモジュールと仲良くなりたい!

それでもEnumモジュールと仲良くなりたい、引き出しを増やしておきたい気持ちは抑えがたいものがあるとおもいます。

オススメの方法は3つです。

  1. AtCoderを解く
  2. Elixirのソースコードを読む
  3. 自分で実装してみる

以下、触りをご紹介します。

1. AtCoderを解く

AtCoderのABCコンテストのC問題くらいまでを解いてみることをオススメします。
Enumモジュールと仲良くなれます。

Enumモジュールと仲良くなることにしぼります。
ABCコンテストのC問題(かD問題)くらいまでを解いてみることで、あんなことをやりたい、こんなことをやりたいという視点で眺めるようになり、Enumモジュールと急接近できます。
急に仲良くなれます。
私はこの方法で仲良くなりました。
ドキュメントのサンプルだけを見ていても想像しにくかったものが、自身の「やりたい」ことを貪欲に探すという見方となり、見えてくる景色が変わります。

もちろん、AtCoderの問題は全部解けたほうがそれはいいに決まっています。
何の自慢にもなりませんが、いまの私はけっこうな時間をかけてやっとD問題が解けるか、解けないかくらいのレベルしかありません。
D問題以上というのは、競技プログラミングの訓練が必要だとおもっています。
Enumモジュールと仲良くなるという目的に絞ったときに、C問題くらいまでが時間と労力のちょうどいいバランスがとれるレベルだと私はおもっています。

繰り返しておきますが、全部解けたほうがいいし、私はその全部解ける域に憧れは感じています。
もうひとつ言うと、解けない自分に危機感は感じています。
生まれたときからコンピュータに囲まれていて、これから新入社員として入ってくる人たちは当然のように全部解いてくる世界が近づいているのだと感じています。

いろいろ書きました。
もう一度話を戻します。
AtCoderのABCコンテストのC問題くらいまでを解いてみることをオススメします。
結局のところ、少し前で説明したGrep結果に挙げられた関数を主に使うことになるとおもいます。

以下、手前味噌な記事ですが参考リンクを貼っておきます。

2. Elixirのソースコードを読む

@piacerex ピアちゃん「GitHubで元のソースコードもガンガン読んでいきましょー😆

スクリーンショット 2022-03-20 9.30.45.png

ドキュメントの右端にひっそりと佇む</>迷わず押してみましょう
押せばわかるさ!
ありがとう!!!

3. 自分で実装してみる

これは @pojiro さんがやられていました。

私は@pojiro さんの記事を眺めただけで、自分ではやっていませんが、これはきっといい訓練になるとおもいます。


Wrapping up :lgtm::lgtm::lgtm::lgtm::lgtm:

この記事は、Enum.max_by/4の説明をしました。
少しだけ、Enumモジュールと仲良くなる方法をご紹介しました。

Enjoy Elixir:bangbang::bangbang::bangbang:
$\huge{Enjoy\ Elixir🚀}$

以上です。

尚々書

感想です。
書き始めのときはこんなに長くなるとは予想していませんでした。
私は書きながらいくつかの新しい発見を得ました。
書くことを楽しみました。


I organize autoracex.
And I take part in NervesJP, fukuoka.ex, EDI, tokyo.ex, Pelemay.
I hope someday you'll join us.

We Are The Alchemists, my friends!

  1. @kaizen_nagoya さんの「「@e99h2121 アドベントカレンダーではありますまいか Advent Calendar 2020」の改訂版ではありますまいか Advent Calendar 2022 1日目 Most Breakthrough Generator」から着想を得て、模倣いたしました。

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
What you can do with signing up
1