LoginSignup
4

Elixir Tips

Last updated at Posted at 2020-01-11

すぐ忘れる・困った・ハマった・調べるのに苦労した・最初に知りたかったポイントを雑多にまとめた自分用メモ。随時更新。

  • 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/1nil を渡すと FunctionClauseError が、byte_size/1nil を渡すと 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/1Float.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]

正規表現

Regex

細かいフラグも多く柔軟性は高そうだ。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/1List.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

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 の throwrescue ではキャッチできず、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"

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
4