すぐ忘れる・困った・ハマった・調べるのに苦労した・最初に知りたかったポイントを雑多にまとめた自分用メモ。随時更新。
- Erlang/OTP 22
- Elixir 1.9.2
(一部の用語などは適当。あまり裏を取っていないので注意)
IEx
ctrl+D でシェルを終了したい
できない。Erlang にその機能がない上に、実装される予定もない。
https://github.com/elixir-lang/elixir/issues/7310
https://github.com/erlang/otp/pull/983
recompile
アプリ全体をリコンパイルできる。ただしアプリ自体は再起動せず、あくまでもコードをリコンパイルするだけであることに注意。シェル上で関数を実行しながら開発・デバッグする際に使うとよい。
iex> recompile
iex> recompile; MyProject.do_something()
履歴を残す
IEx シェルの履歴を残したい場合は環境変数 ERL_AFLAGS
を設定すればよい。
export ERL_AFLAGS="-kernel shell_history enabled"
出力に色を付ける
vi ~/.iex.exs
IEx.configure colors: [ enabled: true, eval_result: [ :cyan, :bright ] ]
プロンプトを変更する
プロジェクト直下の .iex.exs
へ追記する。
IEx.configure(
default_prompt: "%prefix(%counter) my-app>",
alive_prompt: "%prefix(%node)%counter my-app>"
)
プロジェクト毎にデフォルトで実行する処理を指定する
プロジェクト直下に .iex.exs
を用意する。
vi .iex.exs
デバッグ時によくつかうモジュールを読み込んでおくと便利。
import :timer
import Myproject.Util
alias Myproject.Repo
alias Myproject.{User, Blog, Article}
結果値を省略せずすべて表示する
iex> IEx.configure(inspect: [limit: :infinity])
稼働中のプロセスへ IEx をアタッチする
まず、名前を付けてプロセスを起動する。ここでは app
とした。
elixir --sname app --cookie mysecretcookie -S mix run --no-halt
--cookie
の値を指定しない場合は ~/.erlang.cookie
が作成され使われるようだ。
例えば exenv
+ systemd
なら次のようになるだろう。
(場合によっては MIX_HOME
もセットする必要があるかもしれない)
Environment = MIX_ENV=prod
Environment = PATH=/opt/erlenv/shims:/usr/local/bin:/usr/bin:/bin
WorkingDirectory = /path/to/app
ExecStart = /opt/exenv/shims/elixir --sname app --cookie mysecretcookie -S mix run --no-halt
別の iex を立ち上げ、接続したいプロセス名を --remsh
オプションで指定して接続する。
ここでは IEx
プロセス自身に console
という名前を付けている(省略不可)。
iex --sname console --cookie mysecretcookie --remsh "app@`hostname -s`"
Docker 環境
--sname
の代わりに --name app@localhost
として起動する。
CMD ["elixir", "--name", "app@localhost", "--cookie", "mysecretcookie", "-S", "mix", "run", "--no-halt"]
同様に --remsh
に渡せば接続できる。
docker compose exec myapp iex --sname console --cookie mysecretcookie --remsh app@localhost
IO.inspect
ラベル
出力にラベルを付けられるのでデバッグ時に便利。
iex> [1, 2, 3]
...> |> IO.inspect(label: "before")
...> |> Enum.map(&(&1 * 2))
...> |> IO.inspect(label: "after")
...> |> Enum.sum
before: [1, 2, 3]
after: [2, 4, 6]
12
省略せずにすべての値を表示する
デフォルトは 50 となっているが、そのリミットに :infinity
を指定する。
1..500 |> Enum.map(& &1 * 2) |> IO.inspect(limit: :infinity)
Kernel
自作の関数名がカーネルの関数名とぶつかる場合
たとえば自作の trunc/1
を実装した場合にはカーネルの trunc/1
がすでに存在するのでエラーになる。そこで、カーネルの trunc/1
だけをインポートしないようにして回避できる。
import Kernel, except: [trunc: 1]
Kernel.inspect
tuple や map などを文字列表現にしたい時に使える。Logger に渡す時に便利。
require Logger
iex> data = {:hello, "world"}
iex> Logger.debug("#{inspect(data)}")
18:20:03.730 [debug] {:hello, "world"}
IO.inspect
とは別モノなので混同しないよう注意。
デバッグ情報
observer を起動する
GUI のアプリ稼働状況画面を開く。
iex> :observer.start
値を見る
手っ取り早くプリントする。nil
を渡しても問題ない。
IO.puts("hello")
IO.inspect(%{foo: 42})
nil の扱い
nil かどうか
is_nil(nil) # -> true
is_nil("") # -> false
ブロックの返値
ブロックが値を明示的に返さない場合は nil
が返る。これは Ruby と同じ。
@spec run() :: non_neg_integer | nil
def run do
100
if false do
42
end
end
run() # -> nil
nil ガード?
(正式な機能名はわからない)
result = nil || 42 # -> 42
文字列
Elixir には文字列という概念はなく、すべてバイナリ列である。よって、is_string
といったものは存在しない。一般的な意味での文字列かどうかを調べるには is_binary/1
を使う。
is_binary("hello") # -> true
is_binary(<<102>>) # -> true
また、"hello"
は Elixir の文字列(バイナリ列)だが 'hello'
は Erlang の文字列である。厳密に使い分けること。
is_binary("hello") # -> true
is_binary('hello') # -> false
これらを比較したい場合は ==
ではなく String.equivalent?/2
を使うこと。
"hello" == 'hello' # -> false
ただし 1.11 までは String.equivalent?/2
で両者を直接比較できたが、1.12 以降は FunctionClauseError となるので注意。事前に変換すること。
String.equivalent?('this', "this") # -> true(1.11 以前)
String.equivalent?('this' |> to_string(), "this") # -> true
文字列としてのバイト列は数値のリストとも取れるので、IEx 上で次のようなことが起きてびっくりするだろう。
iex> [65, 66, 67, 68, 69] # -> 'ABCDE'
つまり、ビットの羅列をどう解釈するかという問題。
バイトのリストとしてそのまま表示もできる。
iex> [65, 66, 67, 68, 69] |> IO.inspect(charlists: :as_lists)
文字列として扱えるかどうかを検査する
String.valid?("hello") # -> true
String.valid?("") # -> true
String.valid?('world') # -> false Erlang 文字列。要注意
String.valid?(nil) # -> false
String.valid?(100) # -> false
String.valid?(<<128, 129, 130>>) # -> false
Erlang との相互変換
Erlang 製ライブラリへ値を渡す時などに変換を要する場面が多々ある。
"hello" |> String.to_charlist() # -> 'hello'
Erlang 文字列から Elixir 文字列へ変換するには単純に Kernel.to_string/1
する。
'world' |> to_string() # -> "world"
空かどうか
length/1
が手っ取り早いが、最適解ではないかもしれない。
empty? のようなものは(なぜか)ない。
String.length("hello") == 0 # -> false
サイズ
マルチバイト文字列に対して実行すると両者で結果が違うことに注意。
"こんにちは" |> String.length() # -> 5
"こんにちは" |> byte_size() # -> 15
ただし、String.length/1
に nil
を渡すと FunctionClauseError
が、byte_size/1
に nil
を渡すと ArgumentError
が発生するので注意。
連結
"hello" <> " " <> "world" # -> "hello world"
"hello" |> Kernel.<>(" ") |> Kernel.<>("world") # -> "hello world"
インターポレーション
Ruby ライクに可能。
{foo, bar} = {"result", 42}
"#{foo} is #{bar}" # -> "result is 42"
値が nil
であっても問題ない。
foo = nil
"#{foo}" # -> ""
フォーマット
sprintf のようなものはない。Erlang の :io.format
などを駆使すればできるようだが、必要なら素直に ExPrintf のようなライブラリを使うのがよさそう。
chomp
chomp はないので代わりに String.trim/1
を使う。連続している改行文字を削除する。
"hello\n" |> String.trim() # -> "hello"
"hello\n\n\n" |> String.trim() # -> "hello"
含まれるか
"hello" |> String.contains?("e") # -> true
"this is a pen" |> String.contains?("is a") # -> true
リストを渡すと、(AND
ではなく)OR
で検査される。
"hello" |> String.contains?(["e", "o"]) # -> true
"hello" |> String.contains?(["e", "x"]) # -> true
"hello" |> String.contains?(["x", "y"]) # -> false
これを AND
で検査したければ一手間かかるだろう。
["e", "x"] |> Enum.all?(fn p -> "hello" |> String.contains?(p) end) # -> false
数値へ変換
変換できなければ ArgumentError
が返る。
"42" |> String.to_integer() # -> 42
"42.1" |> String.to_float() # -> 42.1
"foo" |> String.to_integer() # ArgumentError
nil |> String.to_integer() # ArgumentError
"" |> String.to_integer() # ArgumentError
数値かどうか怪しいものは Integer.parse/1
や Float.parse/1
で検査するのがよい。
また、空文字列を渡すと(0 への暗黙の変換はなく)エラーになるので、空になる可能性があるなら必ず検査すること。つまり、外部(ユーザ)からの入力値を変換する際はほとんどの場合で常にチェックする必要があるだろう。
str = "foo"
result =
case Integer.parse(str) do
{n, _} when is_integer(n) -> n
:error -> 0
end
変換を要する機会は多いので次のような簡易的なラッパー関数を用意することにした。
@spec integerize(String.t()) :: integer | nil
def integerize(str) when is_binary(str) do
case Integer.parse(str) do
{n, _} when is_integer(n) -> n
:error -> nil
end
end
@spec floatize(String.t()) :: float | nil
def floatize(str) when is_binary(str) do
case Float.parse(str) do
{f, _} when is_float(f) -> f
:error -> nil
end
end
"200" |> integerize || 0 # -> 200
"two hundreds" |> integerize || 0 # -> 0
Atom へ変換
"foo" |> String.to_atom() # -> :foo
文字コード変換
iconv
もしくは eiconv
を入れる。
{:iconv, "~> 1.0.10"}
:iconv.convert("shift_jis", "utf-8", "なにかSJISの文字列") # -> sjis から utf-8 へ
存在しないエンコーディング(空文字列を含む)を渡しても黙って元の文字列を返すだけなので注意。
昔はエンコーディングは case insensitive だったが今試すと sensitive のようだ。
有効なエンコーディングの一覧はシェルで次のようにすれば確認できる。
iconv -l
Unicode 関連
(そのうち調べる)
:unicode.characters_to_nfd_binary("だ") == "だ" # -> false
UTF-8 の BOM を除く
文字列の先頭にある BOM を除く。BOM が存在しない場合はなにも起こらない。
str |> String.replace_prefix("\uFEFF", "")
最初の文字だけ大文字にする
PHP でいうところの ucfirst
と同等の処理。
def upcase_first(<<first::utf8, rest::binary>>) do
String.upcase(<<first::utf8>>) <> rest
end
その他、大文字小文字などのケースを変換したい場合は ProperCase などを使うとよい。
Atom
定義する
:foo
:"my-project" # 記号などが含まれる場合はクォートする
チェック
is_atom(:foo) # -> true
is_atom("foo") # -> false
いくつかの定義値などは atom で実装されているため?次のようになるので注意すること。
is_atom(nil) # -> true (!) なので注意
is_atom(true) # -> 同上
is_atom(false) # -> 同上
こういった罠のため、is_atom
は基本的に使わないようにすべきだろう。とくに、nil
になる可能性のある変数に is_atom
を使ってはいけない。見つけにくいバグを埋め込むことになる。
文字列へ変換
:foo |> Atom.to_string() # -> "foo"
数値
文字列へ変換
事前に is_integer/1
等で型をチェックしてから実行すること。
42 |> Integer.to_string() # -> "42"
42.1 |> Float.to_string() # -> "42.1"
"123" |> Integer.to_string() # -> ArgumentError
無限の扱い
Infinity, INF といった値はない。
累乗
4 |> :math.pow(3) # -> 64.0
整数で欲しければ Kernel.round/1
する。
4 |> :math.pow(3) |> round # -> 64
算術演算子をパイプする
100 |> Kernel.+(5) # -> 105
乱数を作る
:rand.uniform() # -> 0.0 =< X < 1.0 の少数を返す。0.0 が不要なら uniform_real を参照
:rand.uniform(5) # -> 1..5 の範囲の整数を返す
unique_integer()
:erlang.unique_integer() # -> -576460752303423358
ランタイムインスタンスごとにユニークな整数値を返す、とあるが有効に使える場面があるのかわからない。
カンマ区切り
number を使う。
{:number, "~> 1.0.1"}
12345678 |> Number.Delimit.number_to_delimited # -> "12,345,678.00"
12345678 |> Number.Delimit.number_to_delimited(precision: 0) # -> "12,345,678"
適当にこんな関数を用意している。
@spec commify(number) :: Strint.t()
def commify(n) when is_number(n) do
precision =
case n do
n when is_integer(n) -> 0
n when is_float(n) -> 2
end
Number.Delimit.number_to_delimited(n, precision: precision)
end
Range
1..3 # -> 1..3
3 in 1..5 # -> true
1..3 |> Enum.sum() # -> 6
無限リストを扱いたい時は Stream が使える。
Stream.iterate(0, &(&1 + 1))
|> Stream.map(&(&1 * 2))
|> Enum.take(5) # -> [0, 2, 4, 6, 8]
正規表現
細かいフラグも多く柔軟性は高そうだ。PCRE 互換。
コンパイルする
文字列を正規表現へコンパイルする。compile!/1
もある。
Regex.compile("ab+c") # -> {:ok, ~r/ab+c/}
Regex.compile("???!!??") # -> {:error, {'nothing to repeat', 0}}
ただし、次のようなケースではバックスラッシュを自前でエスケープしておく必要があるようだ。
Regex.compile("\\d+") # -> {:ok, ~r/\d+/}
が、こちらはエスケープなしでいける(条件がよくわからない)。
Regex.compile("[\s\t]+") # -> {:ok, ~r/[ \t]+/}
マッチするかどうか
Regex.match?(~r{o+}i, "foo") # -> true
マッチさせる(最初の要素のみ)
最初にマッチした文字列が返る。
Regex.run(~r{o+}i, "foo fooooo!") # -> ["oo"]
Regex.run(~r{o+}i, "bar") # -> nil
マッチした箇所を返すよう指定できる。
Regex.run(~r{o+}i, "accde foo", return: :index) # -> [{7, 2}]
マッチさせる(すべて)
run/2
のマルチ版だと思えばよい。
Regex.scan(~r{o+}i, "foo fooooo!") # -> [["oo"], ["ooooo"]]
Regex.scan(~r{o+}i, "foo fooooo!", return: :index) # -> [[{1, 2}], [{5, 5}]]
置き換える
Regex.replace(~r{^(.+)@(.+)$}, "me@example.com", "\\1 at \\2") # -> "me at example.com"
デフォルトは greedy match なので、初回マッチのみに限定したければ global 値をセットする。
Regex.replace(~r{fo+}, "foo fooo! foooo!!", "bar") # -> "bar bar! bar!!"
Regex.replace(~r{fo+}, "foo fooo! foooo!!", "bar", global: false) # -> "bar fooo! foooo!!"
関数に渡せるので柔軟に処理できる。
Regex.replace(~r{^(.+)@(.+)$}, "me@example.com", fn _, user, domain -> "#{user} at #{domain}" end)
分割する
正規表現を使って文字列をリストに分割できる。
Regex.split(~r/[\s\t]+/, "aa bb \t cc dd") # -> ["aa", "bb", "cc", "dd"]
タプル
定義する
tuple = {:ok, 42, "hello"}
is_tuple(tuple) # -> true
要素を得る
添字は 0 から始まる。
tuple |> elem(0) # -> :ok
サイズを得る
tuple |> tuple_size # -> 3
要素を置き換えた tuple を返す
tuple |> put_elem(2, "bonjour") # -> {:ok, 42, "bonjour"}
リストへ変換する
{"127", "0", "0", "1"} |> Tuple.to_list # -> ["127", "0", "0", "1"]
リスト
空かどうか
[] |> Enum.empty? # -> true
[1, 2, 3] |> Enum.empty? # -> false
要素の数(長さ)
[] |> Enum.count # -> 0
[1, 2, 3] |> Enum.count # -> 3
多次元配列の場合は List.flatten/1
したい場合もあるだろう。
["a", ["x", "y"], "b", "c"] |> Enum.count # -> 4
["a", ["x", "y"], "b", "c"] |> List.flatten |> Enum.count # -> 5
値が含まれるかどうか
3 in [1, 2, 3] # -> true
[1, 2, 3] |> Enum.member?(3) # -> true
head / tail
以下は同様に使える(パフォーマンスは違うかもしれない)。
[n | tail] = [1, 2, 3] # n -> 1
hd [1, 2, 3] # -> 1
List.first [1, 2, 3] # -> 1
ただし、リストが空の時は例外が発生するので注意が必要。
[n | tail] = [] # -> ArgumentError
hd [] # -> ArgumentError
first / last
List.first/1
や List.last/1
を使うと例外は起きないが:
List.first [] # -> nil
List.last [] # -> nil
やはり値そのものが nil の場合は区別が付かないので、場合によっては事前にサイズを確かめるなどの措置が必要だろう。一長一短。
List.first [] # -> nil
List.first [nil] # -> nil
Set へ変換
[4, 1, 2, 3, 1, 4, 5, 2, 3, 5] |> Enum.into(MapSet.new) # -> #MapSet<[1, 2, 3, 4, 5]>
tuple へ変換
["127", "0", "0", "1"] |> List.to_tuple # -> {"127", "0", "0", "1"}
関数いろいろ
ary = [10, 20, 30]
ary2 = List.insert_at(ary, 1, 15) # -> [10, 15, 20, 30]
Enum.at(ary, 2) # -> 30
Enum.count(ary) # -> 3
Map
特徴
キーにどんな値も指定できる
タプルも空文字列も nil
も正規表現も Map も無名関数ですら指定できる。若干キモい。
%{{:foo, :bar} => 42}
%{"" => 42}
%{nil => 42} # -> %{nil: 42}
%{~r/o+/i => 42}
%{%{foo: "bar"} => 42}
%{fn -> "hello" end => 42} # -> %{#Function<21.126501267/0 in :erl_eval.expr/5> => 42}
キーの順序は保証されない
注意のこと。
キーが atom の場合はドット記法が使える
m = %{ one: 1, two: 2 }
m.one # -> 1
存在しないキーを指定すると
キーが存在しなくても例外は発生しない。単純に nil
となる。
m = %{}
m[:does][:not][:exist] # -> nil
そのため、例えばテキストを期待しているなら次のようにするのがよいのだろう。
if is_binary(param[:my_setting][:user_name]) do
...
end
case param[:my_setting][:user_name] do
user_name when is_binary(user_name) -> ...
nil -> ...
end
あるいは、値を直接得るには Kernel.get_in
を使う。
%{10 => %{apple: "red"}} |> Kernel.get_in([10, :apple]) # -> "red"
ちなみに Ruby での #dig
と同等。
m.dig(:does, :not, :exist) # -> nil
ただし、値に nil
が含まれる場合には、キーが存在しなかったのか値が nil
だったのかの区別はつかない。
m = %{foo: %{bar: nil}}
result = m[:foo][:bar] # -> nil
そのため、基本的にはキーの存在をきちんとチェックすべきだろう。
キーの存在をチェック
キーの存在をチェックするには Map.has_key?/1
を使う。
%{foo: 42} |> Map.has_key?(:foo) # -> true
%{} |> Map.has_key?(:foo) # -> false
マップのサイズを得る
Kernel.map_size/1
を使う。
%{first: 20, second: 30} |> map_size # -> 2
リストから Map を生成する
キーの順序は保証されない点に注意。
list = [{"foo", 1}, {"bar", 2}]
list |> Enum.into(%{}) # -> %{"bar" => 2, "foo" => 1}
キーワードリストからも生成できる。
list = [{:foo, 1}, {:bar, 2}]
list |> Enum.into(%{}) # -> %{bar: 2, foo: 1}
リストからペアを取り出して Map を生成する
["a", "b", "c", "d"]
|> Enum.chunk_every(2)
|> Enum.into(%{}, fn [k, v] -> {k, v} end) # -> %{"a" => "b", "c" => "d"}
すこし複雑な例。
"foo=bar\nbaz=42\n"
|> String.split(~r/\n+/)
|> Enum.filter(fn s -> s != "" end)
|> Enum.map(fn line ->
line
|> String.split("=", parts: 2)
end)
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
# -> %{"baz" => "42", "foo" => "bar"}
やはりキーの順序は保証されないことに注意。
キーとバリューをひっくり返す
@spec invert(map) :: map
def invert(map) when is_map(map) do
for {k, v} <- map, into: %{}, do: {v, k}
end
%{ one: 1, two: 2 } |> invert() # %{1 => :one, 2 => :two}
(かつてバリューであった)キーの重複(による予期しない上書き)には注意。
%{ one: 1, two: 2, zwei: 2 } |> invert() # %{1 => :one, 2 => :zwei}
バリューでソートする
順序を維持するため、キーワードリストへ変換される。
%{ two: 2, one: 1 }
|> Enum.sort(fn(a, b) -> elem(a, 1) < elem(b, 1) end)
# -> [one: 1, two: 2]
バリューを加工する
map_values はないので次のようにする。
for {k, v} <- %{one: 10, two: 20}, into: %{}, do: {k, v + 5} # -> %{one: 15, two: 25}
再帰的にマージする
複雑な構造の Map を再帰的にマージしたい場合は deep_merge を使うとよい。
{:deep_merge, "~> 1.0"}
%{a: 1, b: [x: 10, y: 9]}
|> DeepMerge.deep_merge(%{b: [y: 20, z: 30], c: 4})
各キーの Atom を文字列へ変換する
例えば JSON を処理する一部のライブラリなどは、キーが文字列であることを前提にするケースがある。
@spec atoms_to_keys(map) :: map
def atoms_to_keys(map) when is_map(map) do
map |> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
end
%{foo: 42} |> atoms_to_keys # -> %{"foo" => 42}
recursive に処理したい場合は別のソリューションが必要になるだろう。
各キーのケース(大文字小文字)を変更する
recase を使う。
{:recase, "~> 0.5"}
例えば JSON 用にキャメルケースへまとめて変換するなら、次のような関数を用意しておくと便利。
@spec camelize_keys(map) :: map
def camelize_keys(map) when is_map(map) do
map |> Recase.Enumerable.convert_keys(&Recase.to_camel/1)
end
%{serial_no: 42} |> camelize_keys # -> %{serialNo: 42}
Struct
定義する
defmodule Point do
defstruct x: 10, y: 20
end
作成する
%Point{} # -> %Point{x: 10, y: 20}
%Point{x: 500} # -> %Point{x: 500, y: 20}
Map へ変換する
%Point{} |> Map.from_struct # -> %{x: 10, y: 20}
Set (MapSet)
基本
set = MapSet.new([4, 1, 2, 3, 1, 4, 5, 2, 3, 5]) # -> #MapSet<[1, 2, 3, 4, 5]>
値を追加/削除する
Map のように振る舞えるので put で追加できる。
set2 = set |> MapSet.put(100) # -> #MapSet<[1, 2, 3, 4, 5, 100]>
結果値は常にソートされている、ように見えるがマニュアルに記述はない。
削除する場合は delete する。
set |> MapSet.delete(3) # -> #MapSet<[1, 2, 4, 5]>
値が含まれるかどうか
リストと同じようにチェックできる。
3 in set # -> true
set |> MapSet.member?(3) # -> true
セットどうしをマージする
set |> MapSet.union(set2)
リストへ変換
set |> MapSet.to_list # -> [1, 2, 3, 4, 5]
関数
再帰による繰り返し処理
(多くの場合は)入力として逐次処理したいリストと結果の初期値を渡す。
defmodule Main do
# リストの先頭の値を取り出し、結果値を加工する。リストの残りを自分自身に処理させる
def foo([head | tail], result) do
foo(tail, result + String.length(head))
end
# リスト内のすべての値を処理したら、結果を返す
def foo([], result), do: result
end
Main.foo(["hello", "world"], 0)
結果値(アキュムレータ)は複数持てるので、中間値として使い複雑な処理が書けるが・・
def foo([head | tail], n, cnt, tmp1, tmp2, total, result), do ...
処理したいリスト(等)が複数ある場合はそれぞれ工夫が必要だ(これは手続き型言語の while 文でも同じ)。
別の例。単語の出現回数を再帰で数える。
defmodule Main do
# リストに中身があるうちはこちらにマッチし、先頭の値が name に代入される
@spec countup(list(String.t()), map) :: map
def countup([name | tail], result) do
# 現在の数字
count = result[name] || 0
# result の値を加工する(ここでは数を追加した)
countup(tail, result |> Map.put(name, count + 1))
end
# すべてのリストを処理した場合はこちらにマッチし、結果を返す
def countup([], result), do: result
end
["eeny", "eeny", "meeny", "miny", "miny", "miny", "moe", "moe"]
|> Main.countup(%{})
# -> %{"eeny" => 2, "meeny" => 1, "miny" => 3, "moe" => 2}
複数の when ガード
when ガードを単純に複数回繰り返して記述すると、ロジックは OR
となるようだ。つまり、この場合は引数の型がリストまたはバイナリの場合にマッチする。
def do_something(data) when is_list(data) when is_binary(data) do
...
end
AND にしたければ普通に and で繋ぐ。
def do_something(one, two) when is_struct(one) and is_struct(two) do
...
end
無名関数
基礎
引数なしの無名関数を作る。
f =
fn ->
42
end
f.() # -> 42
仮引数は ()
で指定する。map
等は {}
なので少し紛らわしい。
f = fn (a, b) -> a + b end
ブロック内で外の値と同じものを指定するとローカライズされる。一般的な LL 言語と同様。
n = 10
f = fn n -> n end
f.(20, nil) # -> 20
n # -> 10
省略記法
&
記号を使って簡潔に書ける。
["aa\nbb", "cc\ndd\nee"] |> Enum.flat_map(&(String.split(&1, "\n")))
# こちらと同じ意味
["aa\nbb", "cc\ndd\nee"] |> Enum.flat_map(fn s -> String.split(s, "\n") end)
制御構文
else if はない
else if
はない。代わりに cond
を使う。
cond
cond
はすでに存在する複数の値を使った判定ができる。過去指向。
{n, m} = {100, 150}
result =
cond do
n > 100 -> "n is big"
m > 100 -> "m is bigger"
true -> "n nor m is not so big"
end
- この例のように、結果値を返すこともできる
- 最初にマッチした行が評価され、以降の行は実行されない
- すべてキャッチするデフォルト(
else
とも言える)はtrue
を指定する
効果的に使える場面は意外と限られる。単純なケースでは if/else
で書き換えができるケースも多い。
case
case
はこれから計算する結果値を使って条件分岐したい時に使う。未来指向。
func = fn -> "hello" end
result =
case func.() do
str when is_nil(str) -> "empty!"
str when is_binary(str) and str == "hello" -> "yeah!"
_ -> "oops!"
end
- この例のように、結果値を返すこともできる
- 最初にマッチした行が評価され、以降の行は実行されない
- すべてキャッチするデフォルト(
else
とも言える)は_
を指定する
with
複数のマッチングを束ねる文法。あまり直感的でもないので使いすぎ注意。
with {:ok, content} when is_binary(content) <- read_from_file(file),
{:ok, _result} <- check_content(content) do
true
else
{:error, :invalid} ->
Logger.warn("invalid content: #{file}")
false
{:error, _} ->
false
end
モジュール名・関数
文字列をモジュールに変換する
"MyApplication.HeavyTask" |> Macro.camelize() # -> MyApplication.HeavyTask
モジュール名・関数名を得る
モジュール名と関数名を指定して実行する。
{myfunc, _} = __ENV__.function
result = apply(__ENV__.module, myfunc, ["hello", "world"])
再帰関数を定義する際に、関数名を内部にハードコードしたくない時に使えるかもしれない。
def foo([head | tail], result) do
{myfunc, _} = __ENV__.function
apply(__ENV__.module, myfunc, [tail, result + String.length(head)])
end
特定の関数の arity を得る
(失念)
struct から値を抜き出す
パターンマッチさせる。
uri = URI.parse("https://httpbin.org/get")
%URI{path: path} = uri # path -> "/get"
環境変数
System.get_env("HOME") # -> "/Users/you"
System.get_env("HOME_SWEET_HOME") # -> nil
System.fetch_env("HOME") # -> {:ok, "/Users/you"}
System.fetch_env("HOME_SWEET_HOME") # -> :error
System.put_env("HOME_SWEET_HOME", "Tokyo, Japan") # -> :ok
アプリケーションに定数を持つ
好きなファイル名をいくらでも指定できる。
# config/config.exs
import_config "constants.exs"
# config/constants.exs
import Config # elixir >= 1.9
config :myproject, :mystatus, %{
10 => :inactive,
20 => :active
}
次のように取り出せる。
mystatus = Application.get_env(:myproject, :mystatus)
mystatus = Application.compile_env(:myproject, :mystatus) # コンパイル時。get_env よりこちらが推奨される
モジュール変数?に置けるので便利。
defmodule Main do
@mystatus Application.get_env(:myproject, :mystatus)
def run() do
IO.inspect @mystatus
end
end
並列処理
Task.async_stream
async/3 もあるが結果を統合したいなら async_stream が便利だ。
これを使えば、例えば「時間のかかる処理を5件ずつ処理して結果を待つ(統合する)」が可能になる。
1..3
|> Task.async_stream(fn n -> n * 10 end)
|> Enum.to_list # -> [ok: 10, ok: 20, ok: 30]
単純に結果値だけが欲しいなら次のように取れるが・・
[ok: 10, ok: 20, ok: 30] |> Keyword.values # -> [10, 20, 30]
すべてのタスクが成功したかどうかによって後続の処理を分けたい場合は一工夫必要だろう。
ここでは reduce_while/3
を使って、すべて成功した場合は {:ok, [10, 20, 30]}
を、失敗した場合は {:error, reason}
を返すようにしてみた。
[ok: 10, ok: 20, ok: 30]
|> Enum.reduce_while({:ok, []}, fn elem, {:ok, acc} ->
case elem do
{:ok, value} -> {:cont, {:ok, acc ++ [value]}}
{:exit, reason} -> {:halt, {:error, reason}}
end
end) # -> {:ok, [10, 20, 30]}
返値が必要なければ Enum.to_list/1
の代わりに Stream.run/0
を呼ぶとよい。
タイムアウトのデフォルト値は 5 秒と短いので指定するとよい。最大同時実行数も指定できる。
import :timer
1..3 |> Task.async_stream(fn n -> n * 10 end,
max_concurrency: System.schedulers_online,
timeout: seconds(30),
on_timeout: :kill_task
)
一般的に System.schedulers_online
は CPU コア数となるようだ。
Agent
強引に OOP 言語に例えるなら、「プロパティを1つだけ持てるオブジェクト」的なものを生成する。
基本
start_link で、エージェントの初期値を設定(生成)する。
name を指定しておくと便利。
{:ok, agent} = Agent.start_link(fn -> 42 end, name: :mytest) # -> {:ok, #PID<0.110.0>}
現在の値を get/3 で得る。pid
または name を指定する。
Agent.get(agent, &(&1)) # -> 42
Agent.get(:mytest, &(&1)) # -> 42
タイムアウトも指定できる。デフォルトは 5,000 ms となっている。
Agent.get(:mytest, &(&1), 10_000) # 10秒待つ
内部の値を加工して取り出せる。
Agent.get(:mytest, fn state -> "this is #{state}" end) # -> "this is 42"
内部の値を更新する。
Agent.update(:mytest, &(&1 + 1)) # -> :ok
Agent.get(agent, &(&1)) # -> 43
module にラップする
マニュアルの例そのまま。
defmodule Counter do
use Agent
def start_link(initial_value) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
end
def value, do: Agent.get(__MODULE__, & &1)
def increment, do: Agent.update(__MODULE__, &(&1 + 1))
end
OOP 言語のオブジェクトに近い感じで扱える。
Counter.start_link(100)
Counter.increment # -> :ok
Counter.value # -> 101
Agent.stop/1 で停止できる。これも module に組み込むとよい。
def stop, do: Agent.stop(__MODULE__)
GenServer と同様に supervisor の元で起動するのが一般的なようだ。
children = [
{Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_all)
エラーハンドリング
Erlang の throw 対策
Erlang の throw
は rescue
ではキャッチできず、catch
を使わなくてはならない。
Erlang のすべての例外を一律でキャッチすることもできる。一例。
try do
...
catch
:exit, {:fatal, _} -> ...
end
多くのケースではこのように :exit
を拾えばいいようだが、そのあたりのまとまった仕様 or ベストプラクティスはどこにあるのだろうか。
動的コンパイル
動的言語のようなことができる。
Code.compile_string("defmodule Main do def add(a, b) do a + b end end")
Main.add(10, 20) # -> 30
ログ
メタデータをセット
メタデータは個々に指定することができる。
Logger.debug("hello world", device: "iPhone")
デフォルトのメタデータをセット
そのアプリケーション内のスコープで有効になるメタデータのデフォルト値をセットできる。
もちろん都度上書きもできる。
Logger.metadata(device: "iPhone")
# config/config.exs
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:module, :device]
この例では metadata
に :device
を足している。
ただし async
な処理に回すとメタデータは消失する(ようだ。まだよく理解していない)。
時刻・時間
sleep
Process.sleep(1000) # 単位はミリ秒
import :timer
Process.sleep(seconds(2))
次のような同等の例を見ることもあるかもしれない。
import :timer
:timer.sleep(2000) # 単位はミリ秒
:timer.sleep(seconds(2))
epoch
次のような関数を用意しておくと便利。
defmodule Util do
@spec epoch(atom) :: non_neg_integer
def epoch(unit \\ :second) do
DateTime.utc_now() |> DateTime.to_unix(unit)
end
end
Util.epoch() # -> 1579047584
Util.epoch(:millisecond) # -> 1579047584288
時刻の差を得る
実行にかかった時間を知りたい場合。Timex を使った例。
{:timex, "~> 3.5"}
結果を秒単位で算出するようにしてみた。
use Timex
start = Timex.local()
do_heavy_task()
Timex.diff(Timex.local(), start, :milliseconds) / 1000 # -> 5.782
ベンチマーク
{:benchee, "~> 1.0"},
{:benchee_html, "~> 1.0"},
Benchee.run(
%{
"md5" => fn -> :crypto.hash(:md5, "blah-blah-blah-blah") end,
"sha" => fn -> :crypto.hash(:sha, "blah-blah-blah-blah") end
},
warmup: 1, # 回
time: 2, # 秒
formatters: [
Benchee.Formatters.Console,
{Benchee.Formatters.HTML, file: "./benchmark_result.html"}
]
)
データ
あれこれ。
BaseXX
"hello" |> Base.encode64(case: :lower) # -> "aGVsbG8="
ハッシュ
:crypto.hash(:md5, "hello") |> Base.encode16(case: :lower) # -> "5d41402a...1017c592"
:crypto.hash(:sha512, "hello") |> Base.encode16(case: :lower)
ランダム
secure_random が使える。引数のデフォルトはどれも 16 となっている。
{:secure_random, "~> 0.5"}
SecureRandom.hex(16) # -> "3b60c3c61127c704a5633e14b6e82901"
SecureRandom.base64 # -> "Q1I/swCrSJhJpxsY43S9Ng==
SecureRandom.random_bytes(4) # -> <<44, 71, 133, 248>>
urlsafe_base64()
や uuid()
もある。
型
dializer にかける時の例あれこれ。以下、run()
は入力をそのまま返す関数とする。
文字列
@spec run(String.t()) :: String.t()
def run(str) do
str
end
Elixir の世界では binary
も概ね同じ意味、らしい。
数値
正の整数は non_neg_integer
を使おう。
@spec run(non_neg_integer) :: non_neg_integer
リスト
どちらでも同じ意味。個人的には list()
を使っている。
@spec run(list(String.t())) :: list(String.t())
@spec run([String.t()]) :: [String.t()]
Map
@spec run(map) :: map
@spec run(%{}) :: %{}
時刻
Timex の例。
use Timex
@spec run(DateTime.t()) :: DateTime.t()
def run(time), do: time
run(Timex.local())
nil
nil
が返る可能性がある場合
@spec run(String.t() | nil) :: String.t() | nil
no_return
投げっぱなしで結果値が必要ない場合。
@spec throw_into_blackhole(thingy) :: no_return
モジュール
場合により、いろいろ(元の定義によるようだ、よくわかっていない)。
struct
%Tesla.Env{}
モジュール(〜名としての atom)
Tesla.Adapter.Mint
独自の型
ConCache.Item.t()
Tesla.Env.headers()
モジュール名
dializer 的には atom として認識しているように見える。
defmodule MyProject.Crawler.Google do
...
end
@spec service_module(String.t()) :: atom
def service_module(name) do
case name do
"google" -> MyProject.Crawler.Google
...
end
end
erlang との相互運用
たとえば Phoenix などを使っていると sys.config
などを erlang 文法で書く場面がある。
まとまった資料をまだ見つけていないので手探りで。そのうちちゃんと調べる。
型
bare word を書くと atom になる。
production % -> :production
文字列。
<<"this is a pen">> % -> "this is a pen"