LoginSignup
11
11

More than 5 years have passed since last update.

はじめに

この記事は Elixir Advent Calendar 2016 - Qiita の 23 日目の記事です。

Advent Calender初参加です。文章書くの大変辛いです。

さて、わたしはElixirを書くことが多く、よくどうすればElixirっぽく書けるのかなと思い色々なライブラリーを読んでは発見、読んでは発見の繰り返しをしている時期がありました。

そんな試行錯誤の中見つけたよくやる書き方、Elixirの機能の小ネタなどを私の復習も兼ねてご紹介できればと思っています。紹介する中にはコードの良し悪しで悪い部分があるかもしれません。そちらの評価はみなさんにしてもらえれば幸いです(すみません)。

  • Map & Structs
  • Keyword lists
  • Anonymous functions
  • Kernel in pipeline
  • Comprehension
  • Domain Specific Language(DSL)
  • Built-in Protocol
  • Linter

以下、一つずつ紹介していきます。

Map & Structs

まず基本的なところから。

Elixirは関数型言語なので状態をModule内で動的に保持、扱う事ができません。1

ですのでよく状態をコード内で引き回して扱ったりします2 、こんな風に。

  • 全ての関数にPattern MatchingもGuardも全くない状態
defmodule Response do
  defstruct [:response, :results, :records]

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def results(st) do
    ...

    %{st | results: results}
  end

  def records(st) do
    ...

    %{st | records: records}
  end
end

response = %Response{response: [%{}, %{} ...]}

response
|> Response.results
|> Response.records

これだと何でも通しちゃって問題がある場合もあるので、少し書き換えます。

  • results, records%Response{} のみ通します
defmodule Response do
  defstruct [:response, :results, :records]

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def results(%__MODULE__{} = st) do
    ...

    %{st | results: results}
  end

  def records(%__MODULE__{} = st) do
    ...

    %{st | records: records}
  end
end

もう少し柔軟に、struct を通したい場合は下記のように書けます

  • structを生成すると暗黙的に__struct__がstructのfieldに追加されます
  • __struct__にはResponse Moduleが入ります
defmodule Response do
  defstruct [:response, :results, :records]

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def results(%{__struct__: _} = st) do
    ...

    %{st | results: results}
  end

  def records(%{__struct__: _} = st) do
    ...

    %{st | records: records}
  end
end

もっと柔軟に、struct, map を通したい場合は

  • Keyword listsなどは弾かれる
  • results, records fieldが無いと弾かれる
defmodule Response do
  defstruct [:response, :results, :records]

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def results(%{results: _results} = st) do
    ...

    %{st | results: results}
  end

  def records(%{records: _records} = st) do
    ...

    %{st | records: records}
  end
end

色々応用がききますね

Keyword lists

こちらも基本です。 以下の例ではkey1が取りたいです。

しかしKeyword listsは要素全体がみられるのでこれではエラーが出ます

def do_something([key1: key1]), do: key1

resource = [key1: 1, key2: 2, key3: 3]
IO.inspect AdventCalender.do_something(resource)
# (FunctionClauseError) no function clause matching in AdventCalender.do_something/1

要素全てを定義するとエラーが出ませんが、面倒くさくてもう無理です

def do_something([key1: key1]), do: key1
def do_something([key1: key1, key2: _]), do: key1
def do_something([key1: key1, key2: _, key3: ]), do: key1

resource = [key1: 1, key2: 2, key3: 3]
IO.inspect AdventCalender.do_something(resource)
# 1

リストのPattern Matchingですっきりします

def do_something([{:key1, key1}| _]), do: key1

また、key2, key3が取りたいとき再帰をして値を取り出す方法も考えられると思いますが、あまり無理せずおとなしく Keyword.get/2 などを使用して値だけを渡すか、Keyword listsを使用しないという選択肢が必要なのかもしれません。

Anonymous functions

無名関数は色々な方法で定義することが出来ます。

Partial Application of Function, Name/Arity

関数の部分適用, Name/Arity記法を使用すると簡略な形で無名関数を定義できます。

また関数はcapture operatorの&で束縛できます。

iex(1)> fun1 = &Map.get/2       # Name/Arity
&Map.get/2

iex(2)> fun2 = &Map.get &1, &2  # Partial Application
&Map.get/2

iex(3)> fun3 = & &1[&2]
&Access.get/2

iex(4)> fun1 === fun2
true

iex(5)> fun2 === fun3
false

iex(6)> fun1.(%{key1: 1}, :key1)
1

この性質を利用すると

この形の定義が

iex(1)> Enum.map 0..5, fn num -> is_integer num end
[true, true, true, true, true, true]

こうなって
(Partial Application)

iex(2)> Enum.map 0..5, &is_integer &1
[true, true, true, true, true, true]

こうにもなります
(Name/Arity)

iex(3)> Enum.map 0..5, &is_integer/1
[true, true, true, true, true, true]

応用すると

あまり意味のない例ですが

defmodule Selector do
  def selector(enumerable) do
    Enum.random enumerable
  end

  def choice(enumerable, selector \\ &selector/1) do
    selector.(enumerable)
  end
end

iex(1)> Selector.choice 0..10
3

iex(2)> Selector.choice [0, 0, 1], &Enum.uniq/1
[0, 1]

このようにデフォルトではRandom Choice, 引数にselector関数を渡すことによって振る舞いを変更できる関数になりました

case文の省略

無名関数のPattern Matchingを使用するとcase文を省略できます。

これが

Enum.map enumable, fn element ->
  case element do
    {left, right} ->
      "#{[left, right]}"
    elm when is_atom(elm) ->
      "#{elm}"
    elm when is_number(elm) ->
      "#{elm}"
    num ->
      num
  end
end

こうなります

Enum.map enumable, fn
  {left, right} ->
    "#{[left, right]}"
  elm when is_atom(elm) ->
    "#{elm}"
  elm when is_number(elm) ->
    "#{elm}"
  num ->
    num
end

スッキリしました

Kernel in pipeline

Elixirで普段使用するデフォルトのMacro,FunctionはKernel Module内で定義されています。それらは使用時に自動的に我々の環境にImportされます。(if, unlessなど)

ですのでKernel Module経由でそれらの呼び出しをすることも可能です。

iex(1)> Kernel.<>("My", "View")
"MyView"

iex(2)> "My" <> "View"
"MyView"

iex(3)> Kernel.in(1, [10])
false

iex(4)> 1 in [10]
false

このKernel Moduleを利用するとPipeline処理で一貫した書き方が出来るようになります。

これはあまり良い例が思いつかないですが。。

これが

chose =
  1..100_00
  |> Enum.reduce(&+/2)

IO.inspect chose + 100_00
# 50015000

こうなります

1..100_00
|> Enum.reduce(&+/2)
|> Kernel.+(100_00)

変数にassignする必要がなくなりました。素晴らしいほど小ネタです。

Comprehension

基本的なことですがループ処理・フィルタリング処理・マッピング処理を同時にできる内包表記はとてもパワフルです。

たとえばあるModuleから任意の引数の数をもつ関数を抽出したい場合、内包表記を使わないでド直球に実装したらこんな感じになるでしょうか

def extract_function(modules, arity) do
  Enum.reduce modules, [], fn module, acc ->
    functions =
      Enum.map(module.__info__(:functions), fn
        {function, ^arity} -> function
        _                  -> nil
      end)

    acc ++ Enum.filter_map(functions, & !!&1, & {module, &1})
  end
end

IO.inspect My.extract_function([Enum, Map], 4)
# [{Enum, :chunk}, {Map, :update}]

内包表記を使うと

modules = [Enum, Map]
arity = 4

for module <- modules,
    {function, ^arity} <- module.__info__(:functions) do

  {module, function}
end

とてもシンプルになりました。使えるところには積極的に使っていきたいですね。

Domain Specific Language(DSL)

Elixirに慣れてくるとDSLを書きたくなるときが来るかもしれません。

例えば下記のようなシンプルなDSLがあったとします。

defmodule Definition do
  use Schema

  settings do
    analysis do
      analyzer :ngram_analyzer,
        tokenizer: "ngram_tokenizer",
        char_filter: ["html_strip", "my_iteration_mark"],
        filter: ["lowercase", "my_stemmer", "cjk_width"]
    end
  end
end

iex(1)> Definition.es_analyses
{:analyzer, :ngram_analyzer,
 [tokenizer: "ngram_tokenizer",
  char_filter: ["html_strip", "my_iteration_mark"],
  filter: ["lowercase", "my_stemmer", "cjk_width"]]}

このDSLはこのように書けるかもしれません。

  • use Schema をした時に import Schema される
  • Importによって全てのmacro(settings, analysis, analyzer)が使用可能な状態
defmodule Schema do
  @doc false
  defmacro __using__(_) do
    quote do
      import Schema
    end
  end

  defmacro settings([do: block]), do: block
  defmacro analysis([do: block]), do: block
  defmacro analyzer(name, opts) do
    quote do
      def es_analyses, do: {:analyzer, :"#{unquote(name)}", unquote(opts)})
    end
  end
end

これは analyzer が直接呼べてしまい、settings, analysis が無くても動作してしまいます。また予期せぬ操作を防ぐために、ユーザーにはsettingsだけ触らせたいです。

少し書き換えます。

  • Import対象はsettings/1のみ
  • settingsが呼ばれた時に全てのMacroをImport、その後Blockを展開する
defmodule Schema do
  @doc false
  defmacro __using__(_) do
    quote do
      import Schema, only: [settings: 1]
      Module.register_attribute(__MODULE__, :es_analyses, accumulate: false)
    end
  end

  defmacro settings([do: block]) do
    quote do
      import Schema

      unquote(block)
      """
      # unquote(block)で下記のようなコードが最終的に展開される
      #
      Module.put_attribute __MODULE__, :es_analyses, {
        :analyzer,
        :erlang.binary_to_atom("#{:ngram_analyzer}", :utf8),
        [tokenizer: "ngram_tokenizer", char_filter: ["html_strip", "kuromoji_iteration_mark"], filter: ["lowercase", "kuromoji_stemmer", "cjk_width"]]
      }
      """

      def es_analyses, do: @es_analyses
    end
  end

  # For just imitational function.
  defmacro analysis([do: block]), do: block

  defmacro analyzer(name, opts) do
    quote do
      Module.put_attribute(__MODULE__, :es_analyses, {:analyzer, :"#{unquote(name)}", unquote(opts)})
    end
  end
end

iex(1)> Definition.es_analyses
{:analyzer, :ngram_analyzer,
 [tokenizer: "ngram_tokenizer",
  char_filter: ["html_strip", "my_iteration_mark"],
  filter: ["lowercase", "my_stemmer", "cjk_width"]]}

これでanalyzer, analysisは直接さわれなくなり、望んだ形になりました。

Built-in Protocol

ElixirにはいくつかのBuilt-in protocolsが用意されています、この機能を使用するとEnumの関数,to_stirngなどに独自の振る舞いを追加、統合することができます。(プロトコルの説明はBlank実装で有名なこのページが詳しいです。)
今回はBuilt-in Protocolの中でよく使うであろう、Collectionを扱うEnumerableを紹介したいと思います。

当初登場した、Response Module(Struct)を覚えていますか?少し書き換えてありますがこちらです。

defmodule Response do
  defstruct response: [], records: []

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def records(st) do
    %{st | records: st.response}
  end
end

このResponse Moduleは下記のように使用することを想定しています。

response =
  [%{}, %{}, %{} ...]
  |> Response.wrap
  |> Response.records

data =
  Enum.map response.records, fn record ->
    ...

    converted_value
  end

IO.inspect data
# [[], [], [] ...]

これ自体は何も問題のないコードなのですが、、、あえてさらにResponse Moduleに機能を付け加える、
Enumerable Protocolを使用してResponse StructをCollectionとして扱わせたいと思います。

少し書き換えます。

enum, mapなどで処理をさせたいだけなので count/1, member?/2 は実装していません。
defmodule Response do
  defstruct response: [], records: []

  def wrap(response) do
    %__MODULE__{response: response}
  end

  def records(st) do
    %{st | records: st.response}
  end

  defimpl Enumerable, for: Response do
    def count(_page), do: {:error, __MODULE__}

    def member?(_page, _value), do: {:error, __MODULE__}

    def reduce(%Response{records: records}, acc, fun) do
      Enumerable.reduce(records, acc, fun)
    end
  end
end

response =
  [%{}, %{}, %{} ...]
  |> Response.wrap
  |> Response.records

data =
  Enum.map response, fn record ->
    ...

    converted_value
  end

IO.inspect data
# [[], [], [] ...]

IO.inspect Enumerable.count(response)
# {:error, Enumerable.Response}

ResponseにはEnumerable Protocolを実装してあるので、直接Collectionとして扱われるようになりました。

Linter

local関数は括弧を付けましょうとか、関数の引数は5個までが望ましいとか

そういう細かいところはLinterに任せちゃいましょう

環境:
- https://github.com/rrrene/credo
- https://github.com/vim/vim
- https://github.com/neomake/neomake

Note: Vimスクリプトはよく分かっていませんのでより良く書き換えてください

let g:neomake_elixir_mycredo_maker = {
      \ 'exe': 'mixup',
      \ 'args': ['credo', '--format=flycheck', '--strict'],
      \ 'errorformat':
          \ '%E%f:%l:%c: %m,' .
          \ '%E%f:%l: %m'
      \ }
let g:neomake_elixir_mymix_maker = {
      \ 'exe' : 'mixup',
      \ 'args': ['compile', '--warnings-as-errors'],
      \ 'errorformat':
        \ '** %s %f:%l: %m,'.
        \ '%f:%l: warning: %m'
      \ }

let g:neomake_elixir_enabled_makers = ['mycredo', 'mymix', 'elixir']

function! Mixmeansnothing()
    if !exists('t:cwd')
       let t:cwd = system("mixup pwd")
    endif

    execute 'cd' t:cwd
endfunction

autocmd! BufWritePre * call Mixmeansnothing()
autocmd! BufWritePost * Neomake

mixup部分

#!/usr/bin/env zsh

findup() {
    FILE=$1
    while [ ! -e "${FILE}" ]; do
        if [ "$(pwd)" = "/" ]; then
            return
        fi
        cd ..
    done
    echo "$(pwd)"
}

# usage example
MIX=mix.exs
PDIR=$(findup ${MIX})

if [ "$1" = "pwd" ]; then
    echo $PDIR
    exit 0
fi

if [ "${PDIR}/${MIX}" != "/${MIX}" ]; then
    # echo "(cd ${PDIR}; mix $@)"
    cd ${PDIR}; mix $@
    exit 0
fi

echo "couldn't find file ${MIX}"
exit 1

mycredo

おわりに

簡単ですが以上です、読んでくださって本当にありがとうございました。来年まで文章は書けません。


  1. Agentなどを使用すれば似たような事を実現出来ます 

  2. Ecto, Plugなどが代表例 

11
11
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11