はじめに
この記事は 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に任せちゃいましょう
環境
:
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
おわりに
簡単ですが以上です、読んでくださって本当にありがとうございました。来年まで文章は書けません。