みかの原わきて流るる泉川いつ見きとてか恋しかるらむ
Advent Calendar 2022 77日目1の記事です。
I'm looking forward to 12/25,2022
私のAdvent Calendar 2022 一覧。
はじめに
Elixirを楽しんでいますか
max_by/4がわからん(引数が4つの例) pic.twitter.com/P6ieOmppew
— ymn (@ymnbuild) March 19, 2022
@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
まずは公式ドキュメントを眺めてみます。
第一引数の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
orfalse
を返す関数 -
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
が返せました
一体、これの何がうれしいのでしょうか? とおもった方もいらっしゃるかもしれません。
たとえば以下のように、第一引数の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 ピアちゃんが紹介されていました。
リファレンスが分かりにくいときは、関数名+アリティまんまでググりましょう…下記みたいなコラムが見つかります😌https://t.co/3gcNnYMbAu
— piacere (love Elixir, Gravity and VR/AR/Metaverse) (@piacere_ex) March 19, 2022
それと、Enumヲタとか目指さないので良ければ、使うシーンが出るまで調べない…という方法もあります(だいたいソレでも事足ります)😋
@piacerex ピアちゃん
「リファレンスが分かりにくいときは、
関数名+アリティまんまでググりましょう」
これは、すんごいテクニックです!!!
ググり結果の良記事は以下です。
Enumモジュールと仲良くなりたい!
なるほど🧐「max_by/4」このままで検索するのですね。
— ymn (@ymnbuild) March 19, 2022
全部理解しないとしても、とりあえずはどんな物があるか知っておきたいと思いました。
(引き出しを増やしたい)
必要になったら詳細を調べたいと思います。
わかります。
Enumモジュールと仲良くなるとElixirはますます楽しくなります。
@cooldaemon さんの記事です。
全編オススメです。
この記事と特に関連するのは195ページあたりからです。
さらに1ページだけ絞り込むと、272ページです。
ここに上がっている関数を理解できれていればたいていのことは事足りるようにおもいます。
続・Enumモジュールと仲良くなりたい!
それでもEnumモジュールと仲良くなりたい、引き出しを増やしておきたい気持ちは抑えがたいものがあるとおもいます。
オススメの方法は3つです。
以下、触りをご紹介します。
1. AtCoderを解く
AtCoderのABCコンテストのC問題くらいまでを解いてみることをオススメします。
Enumモジュールと仲良くなれます。
Enumモジュールと仲良くなることにしぼります。
ABCコンテストのC問題(かD問題)くらいまでを解いてみることで、あんなことをやりたい、こんなことをやりたいという視点で眺めるようになり、Enumモジュールと急接近できます。
急に仲良くなれます。
私はこの方法で仲良くなりました。
ドキュメントのサンプルだけを見ていても想像しにくかったものが、自身の「やりたい」ことを貪欲に探すという見方となり、見えてくる景色が変わります。
もちろん、AtCoderの問題は全部解けたほうがそれはいいに決まっています。
何の自慢にもなりませんが、いまの私はけっこうな時間をかけてやっとD問題が解けるか、解けないかくらいのレベルしかありません。
D問題以上というのは、競技プログラミングの訓練が必要だとおもっています。
Enumモジュールと仲良くなるという目的に絞ったときに、C問題くらいまでが時間と労力のちょうどいいバランスがとれるレベルだと私はおもっています。
繰り返しておきますが、全部解けたほうがいいし、私はその全部解ける域に憧れは感じています。
もうひとつ言うと、解けない自分に危機感は感じています。
生まれたときからコンピュータに囲まれていて、これから新入社員として入ってくる人たちは当然のように全部解いてくる世界が近づいているのだと感じています。
いろいろ書きました。
もう一度話を戻します。
AtCoderのABCコンテストのC問題くらいまでを解いてみることをオススメします。
結局のところ、少し前で説明したGrep結果に挙げられた関数を主に使うことになるとおもいます。
以下、手前味噌な記事ですが参考リンクを貼っておきます。
2. Elixirのソースコードを読む
トリッキーなアリティのものは、実際に使える頻度も高くないので、同名の関数は、代表的なアリティを押さえてくと良いですヨー😉
— piacere (love Elixir, Gravity and VR/AR/Metaverse) (@piacere_ex) March 19, 2022
一方で、好奇心やヲタ動機なら止めません…ググり方やGithubで元のソースコードもガンガン読んでいきましょー😆
一番早い解決は、Elixir本体のコードを読む…だったり😝
@piacerex ピアちゃん「GitHubで元のソースコードもガンガン読んでいきましょー😆」
ドキュメントの右端にひっそりと佇む</>
を迷わず押してみましょう。
押せばわかるさ!
ありがとう!!!
3. 自分で実装してみる
これは @pojiro さんがやられていました。
私は@pojiro さんの記事を眺めただけで、自分ではやっていませんが、これはきっといい訓練になるとおもいます。
Wrapping up
この記事は、Enum.max_by/4の説明をしました。
少しだけ、Enumモジュールと仲良くなる方法をご紹介しました。
Enjoy Elixir
$\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!