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.t
1、すなわち[{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
してからパターンマッチするということも可能ではある。美しいかどうかはわからない。 ↩