Elixir Short Tips
(ほぼ)Elixirだけを書いてもうすぐ1年。いくつか短いTipsが浮かんでいるのでまとめる。
自分で書きつつ、コードレビューしつつ、されつつ、今Elixirを書くときに考えるようになった色々なこと。
しかしただ書いてもなんだか味気ないので、「趣き重視」で書く。
「あなたはtupleを比較する」
-
解説
- あまりにも基礎的で、ともすれば見過ごしてしまう明快な事実。
{2016, 12, 31} < {2017, 1, 1} # true-
:calendar.date/0はだから美しい。:calendar.time/0もまた美しい。- 一方で、このようなデータ構造を我々に作らせようと誘惑する魔とは戦うべきだろう。
#XXX Do we REALLY need this? %{year: 2016, month: 12, day: 31}- コード上のkey順序が意味をなさない
mapでは、比較の際にどうなるか、言うまでもない。 - これだけを見れば相当に愚かな例だが、
map、そしてstructに手をかける前、一呼吸入れて損はない。
-
Erlangでは、「全ては比較できる」。同質なモノ同士であれば、例えばここでの同サイズの
tupleのように、その振る舞いは比較的自然に定義されている。- さらに言えば、異質なモノ同士を比較しようとしたとき、どのように振る舞うかでさえ明確に定義されている。
「Enumを見よ。Enumに求めよ」
- 解説
-
Enumに無いものは無い。などということは無い。 - だが
Enum.map/2がある。Enum.map/2のことを考えない日など有るだろうか? - そして
Enum.group_by/3があり、Enum.map_join/3があり、Enum.map_reduce/3があり、Enum.partition/2があり、Enum.filter_map/3がある。 -
数え上げうるものに対して作用したいと思ったとき、
Enumが応えてくれないことは少ない。-
Enumが応えてくれないときは、むしろ己を疑ってもいいとさえ思う。
-
- リンクした記事でも書いたように、
Enum.reduce/3は表現力の高い「ビルドブロック」的関数である。その分、実装に使うと読みづらくなりがちだ。-
Enumが用意している関数の中に用途を満たすものがないか、まず探してからreduce/3を使わなければならないかどうか考えるべきといえる。
-
-
「母なるMix」
-
解説
-
Mix.env/0やMix.Project.config/0など、Mixから情報を引き出すことはできる。だがそれができるのはrelease前だけだ。- 今少し正確に言うと、Mixアプリケーションが起動しているときだけだ。
-
mix testコマンドでテストしたり(MIX_ENV=test)、iex -S mix(MIX_ENV=dev)したりするとき、暗黙にMixアプリケーションも起動していて、mix.exsファイルの中に書かれたConfig情報などは状態として保持している。Mix.env == :sys.get_state(Mix.State) |> Map.get(:env)
- 当然だ。Mix自身がそれを使ってユーザのアプリケーションを世話してくれるのだから。
- しかし、アプリケーションをreleaseの形でまとめた場合は話が変わる。そこには通常、Mix自身は含まれない。
- ユーザのアプリケーションは、runtimeに本当に必要となるアプリケーションだけを従えて、独り立ちする。Mixは送り出すだけだ。母のように。
- Mixを含まない起動命令に基づけば、以下は実行時エラーになる。
defmodule M do def endpoint_url do case Mix.env do :prod -> "https://public.endpoint.com" _ -> "http://localhost:8080" end end end- コンパイル時に使えば良い。母はそこにいる。
defmodule M do @endpoint_url (case Mix.env do :prod -> "https://public.endpoint.com" _ -> "http://localhost:8080" end) def endpoint_url: do: @endpoint_url end -
「Keyword.tの美、mapの罪」
-
解説
SomeModule.some_fun(prop1: 1, prop2: nil, prop3: "Three")- これは
Keyword.t1、すなわち[{atom, any}]を関数に渡すときにのみ許された構文だ。 -
Keyword.tはあくまでlistの特殊型に過ぎず、当然listを扱うのと同じくせねばならない。 - しかし
mapを知ってしまった我々は、時に過ちを犯す。
defmodule M do def list_fun(prop1: p1, prop2: p2) do {p1, p2} end def map_fun(%{prop1: p1, prop2: p2}) do {p1, p2} end end M.list_fun(prop1: 1, prop2: 2) # {1, 2} M.list_fun(prop2: 2, prop1: 1) # ** (FunctionClauseError) no function clause matching in M.list_fun/1 M.list_fun(prop1: 1, prop2: 2, prop3: 3) # ** (FunctionClauseError) no function clause matching in M.list_fun/1 M.map_fun(%{prop2: 2, prop1: 1}) # {1, 2} M.map_fun(%{prop1: 1, prop2: 2, prop3: 3}) # {1, 2}- 実際にここまで愚かなコードを書くことは流石にないだろう。
- だが、
mapの柔軟さに慣らされた者が荒々しく近づけば、美しいKeyword.tは折れてしまうことを知っておかねばならない。- ライブラリ関数の末尾引数として
optionsのようなKeyword.tを取ることはままある。省略記法を綺麗に適用できるため、誘惑に駆られる。 - しかし、翻って関数実装側は
Keyword.tに対して鮮やかにパターンマッチすることはできない。Keyword.get/3の連発になるだろう。2
- ライブラリ関数の末尾引数として
- これは
結び
深夜だなあ。
明日は@ikeyasuさん。
-
Erlangの
proplistsとは、「関数のオプション指定」といった頻出用途で似ているものの、別物である点には注意。 ↩ -
すぐに思いついた例だと、
Regex.split/3のように。一旦変換してデフォルト値の入ったmapとMap.merge/2してからパターンマッチするということも可能ではある。美しいかどうかはわからない。 ↩