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 一覧](https://docs.google.com/spreadsheets/d/1HQvFjagQLRPjOYAjDVzWp9S4b8dKixxvvaz_TtbZWto/edit#gid=1723448955)。

[^1]: @kaizen_nagoya さんの「[「@e99h2121 アドベントカレンダーではありますまいか Advent Calendar 2020」の改訂版ではありますまいか Advent Calendar 2022 １日目　Most Breakthrough Generator](https://qiita.com/kaizen_nagoya/items/49ebebee3a0377f3b59b)」から着想を得て、模倣いたしました。 

---



# はじめに

[Elixir](https://elixir-lang.org/)を楽しんでいますか:bangbang::bangbang::bangbang:

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">max_by/4がわからん（引数が４つの例） <a href="https://t.co/P6ieOmppew">pic.twitter.com/P6ieOmppew</a></p>&mdash; ymn (@ymnbuild) <a href="https://twitter.com/ymnbuild/status/1505079498316054531?ref_src=twsrc%5Etfw">March 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

@t-yamanashi さんのツイートをみました。
[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)の4番目の引数って何だったっけ？　と私はおもいだせませんでした。
そこで調べてみました。

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

## モチベーション

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

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

私は、[Advent Calendar 2022](https://docs.google.com/spreadsheets/d/1HQvFjagQLRPjOYAjDVzWp9S4b8dKixxvvaz_TtbZWto/edit#gid=1723448955)を完成させたく、ネタを探しています。


# [Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)

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

![スクリーンショット 2022-03-20 7.46.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131808/453effcb-472a-1f21-5cf8-73006dd50dfc.png)

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

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

## [Term ordering](https://hexdocs.pm/elixir/operators.html#term-ordering)

[Elixir](https://elixir-lang.org/)は、異なる型どうしの大小比較ができます。
そのルールは、[Term ordering](https://hexdocs.pm/elixir/operators.html#term-ordering)に書いてあります。

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

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

```elixir
iex> 1 < :an_atom
true
```

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

異なる型どうしの大小比較は以降、この記事では取り扱いませんが、[Elixir](https://elixir-lang.org/)の大小比較の特徴ですのでご紹介しておきました。


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

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

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

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

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

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

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

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

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

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

mapを比較してみます。

```elixir
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

想像してください。

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


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

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

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

```elixir
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](https://github.com/elixir-lang/elixir/blob/v1.13.3/lib/elixir/lib/enum.ex#L1797-L1807) が実はある

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

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

と

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

は同じなのです。

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

上記の`fn -> nil end`は、[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)の第三引数かと言うとNoです。
[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)においては、第四引数です。

ふつうは、前から順に埋められていきます。
なぜ、`fn -> nil end`が、[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)の第四引数になり得るかというと、[Enum.max_by/3](https://github.com/elixir-lang/elixir/blob/v1.13.3/lib/elixir/lib/enum.ex#L1797-L1807)に秘密があります。


[Enum.max_by/3](https://github.com/elixir-lang/elixir/blob/v1.13.3/lib/elixir/lib/enum.ex#L1797-L1807)というドキュメントにはないメソッドがあるのです。
その実装をみると以下のようになっています。

```elixir
  @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
```

ね！
これにより、

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

と

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

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

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

https://github.com/elixir-lang/elixir/pull/9427

> 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](https://hexdocs.pm/elixir/Enum.html#max_by/4)の第三引数で`sorter`が追加されたとのことです。



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

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

[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)で示されている例をもとに説明を続けます。

```elixir
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](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison)」に説明が書いてあります。
そこからポイントを抜き出すと、Date構造体そのもの同士の大小比較では、`:day`、`:month`、`:year`の順に比較をすることになるとのことです。
では、私達が望む日付同士の比較をするにはどうしたらよいのでしょうか。
そうです。`sorter`です。

答えを書きます。

```elixir
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`モジュールをつくれば指定できます。


```elixir
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`です。

```elixir
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`を指定する出番です。

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

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

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

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

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

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

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

```elixir
Enum.max_by([], &String.length/1, fn -> nil end)
```
の例は
第1引数、第2引数、第4引数が指定されています。
第3引数はデフォルトの`&>=/2`が使用されます。
ということを記事を書きながら改めて知りました。
なぜこんなことができるのかについてはこの[記事の上のほう](https://qiita.com/torifukukaiou/items/ee38aea95111b505798a#enummax_by3-%E3%81%8C%E5%AE%9F%E3%81%AF%E3%81%82%E3%82%8B)で説明しました。


https://qiita.com/torifukukaiou/items/ee38aea95111b505798a#enummax_by3-%E3%81%8C%E5%AE%9F%E3%81%AF%E3%81%82%E3%82%8B



これで、[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)の説明を終わります。

# 付録

以下、付録です。

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

https://superruzafa.github.io/visual-elixir-reference/Enum/max_by/4/

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

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">リファレンスが分かりにくいときは、関数名+アリティまんまでググりましょう…下記みたいなコラムが見つかります😌<a href="https://t.co/3gcNnYMbAu">https://t.co/3gcNnYMbAu</a><br><br>それと、Enumヲタとか目指さないので良ければ、使うシーンが出るまで調べない…という方法もあります（だいたいソレでも事足ります）😋</p>&mdash; piacere (love Elixir, Gravity and VR/AR/Metaverse) (@piacere_ex) <a href="https://twitter.com/piacere_ex/status/1505116726958047236?ref_src=twsrc%5Etfw">March 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

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

https://superruzafa.github.io/visual-elixir-reference/Enum/max_by/4/

## [Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなりたい！

 
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">なるほど🧐「max_by/4」このままで検索するのですね。<br><br>全部理解しないとしても、とりあえずはどんな物があるか知っておきたいと思いました。<br>（引き出しを増やしたい）<br><br>必要になったら詳細を調べたいと思います。</p>&mdash; ymn (@ymnbuild) <a href="https://twitter.com/ymnbuild/status/1505118630589382657?ref_src=twsrc%5Etfw">March 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

わかります。
[Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなると[Elixir](https://elixir-lang.org/)はますます楽しくなります。

@cooldaemon さんの記事です。

https://speakerdeck.com/cooldaemon/erlang-falsele-sisatoxin-sa

全編オススメです。
この記事と特に関連するのは195ページあたりからです。
さらに1ページだけ絞り込むと、[272ページ](https://speakerdeck.com/cooldaemon/erlang-falsele-sisatoxin-sa?slide=272)です。

![スクリーンショット 2022-03-20 9.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131808/d5b7ecb6-be13-dd5a-3af3-20f77c49f501.png)

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


## 続・[Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなりたい！

それでも[Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなりたい、引き出しを増やしておきたい気持ちは抑えがたいものがあるとおもいます。

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

1. [AtCoder](https://atcoder.jp/?lang=ja)を解く
1. [Elixir](https://elixir-lang.org/)のソースコードを読む
1. 自分で実装してみる

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

## 1. [AtCoder](https://atcoder.jp/?lang=ja)を解く

[AtCoder](https://atcoder.jp/?lang=ja)のABCコンテストのC問題くらいまでを解いてみることをオススメします。
[Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなれます。

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

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

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

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

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

https://zenn.dev/torifukukaiou/articles/ac84c87736ceebf4da01

https://qiita.com/torifukukaiou/items/9a2235d2476857f9ff32

## 2. [Elixir](https://elixir-lang.org/)のソースコードを読む

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">トリッキーなアリティのものは、実際に使える頻度も高くないので、同名の関数は、代表的なアリティを押さえてくと良いですヨー😉<br><br>一方で、好奇心やヲタ動機なら止めません…ググり方やGithubで元のソースコードもガンガン読んでいきましょー😆<br><br>一番早い解決は、Elixir本体のコードを読む…だったり😝</p>&mdash; piacere (love Elixir, Gravity and VR/AR/Metaverse) (@piacere_ex) <a href="https://twitter.com/piacere_ex/status/1505121836668837895?ref_src=twsrc%5Etfw">March 19, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

![スクリーンショット 2022-03-20 9.30.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131808/bace5271-df41-ecc0-a9bd-430d95e21399.png)

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

https://github.com/elixir-lang/elixir/blob/v1.13.3/lib/elixir/lib/enum.ex#L1860





## 3. 自分で実装してみる

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

https://qiita.com/pojiro/items/6d880829ea36c85856bf

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



---

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

この記事は、[Enum.max_by/4](https://hexdocs.pm/elixir/Enum.html#max_by/4)の説明をしました。
少しだけ、[Enum](https://hexdocs.pm/elixir/Enum.html#content)モジュールと仲良くなる方法をご紹介しました。

Enjoy [Elixir](https://elixir-lang.org/):bangbang::bangbang::bangbang:
<font color="purple">$\huge{Enjoy\ Elixir🚀}$</font>


以上です。

## 尚々書

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





---

I organize [autoracex](https://autoracex.connpass.com/).
And I take part in [NervesJP](https://nerves-jp.connpass.com/), [fukuoka.ex](https://fukuokaex.connpass.com/), [EDI](https://fukuokaex.connpass.com/), [tokyo.ex](https://beam-lang.connpass.com/), [Pelemay](https://pelemay.connpass.com/).
I hope someday you'll join us.

[We Are The Alchemists, my friends!](https://www.youtube.com/watch?v=04854XqcfCY)