趣き重視でElixir Tips

  • 16
    Like
  • 0
    Comment

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/0Mix.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さん。


  1. Erlangのproplistsとは、「関数のオプション指定」といった頻出用途で似ているものの、別物である点には注意。 

  2. すぐに思いついた例だと、Regex.split/3のように。一旦変換してデフォルト値の入ったmapMap.merge/2してからパターンマッチするということも可能ではある。美しいかどうかはわからない。