最近仕事でElixir使うようになって覚えた書き方をメモしておきます(version1.5.0)。
(もっと分かってくれば他にも書いていきます)
Elixir本体
構造体のパターンマッチ
def func(%SomeStruct{status: some_status}, arg2, arg3) do
# 何らかの処理
end
関数のヘッド部で構造体のフィールドを変数に束縛させてそれを関数の内部で利用するときに使ってます。
ガードと組み合わせて処理をフィールドの値によって振り分けるときにも使えます。
def func(%SomeStruct{status: some_status}, arg2, arg3) when some_status in [0, 1, 2] do
# 何らかの処理
end
def func(%SomeStruct{status: some_status}, _, _) do
raise("invalid status: #{some_status}")
end
引数の型でパターンマッチもできる(spec書けば不要です)
def func(%SomeStruct{}=arg) do
# 何らかの処理
end
def func(%OtherStruct{}=arg) do
# 何らかの処理
end
Caseとパイプ
使いすぎると可読性が下がりますが関数をパイプでCase式に繋げて処理分岐します。
(例が悪くて申し訳ないです)
# arg = [0, 1, 2, 3, 4]
def func(arg) do
|> Enum.filter(arg, &Integer.is_odd())
|> Enum.count
|> case do
2 -> "何らかの処理"
_ -> "何らかの処理"
end
end
当然ifでも出来ます(が使う機会は今のところありません)
Module.Concat
namespaceをつけてモジュールを宣言するときに使います。モジュールを動的に生成するときに使うことが多いです。
contents = quote do
def func(), do: "Hello Elixir!"
end
Module.create(Module.concat(Elixir, Hello), contents, Macro.env.location(__ENV__))
Hello.func()
"Hello Elixir"
構造体の更新
以下の書き方でフィールドを更新した構造体を再作成出来ます。
defmodule Hello
defstruct [:a, :b, :c, :d]
end
val1 = %Hello{a: 1, b: 2}
# フィールドを更新する
val2 = %Hello{val1|a:3, b: 4}
Mapの場合は元から用意されているupdate/3
かupdate/4
を使うことも多いです
val = %{a: 1, b: 2}
val2 = Map.update(val, :a, &(&1+1))
#=> %{a: 2, b: 3}
val3 = Map.update(val2, :c, 4, &(&1+1))
#=> %{a: 2, b: 3, c:4}
IO.inspect
のラベル
デバッグのときに便利です
arg
|> IO.inspect(label: "before func")
|> func()
|> IO.inspect(label: "after func")
文字列分割
文字列操作で一字ずつ分割したい場合。
(あまり文字列操作する機会ないですが)
String.split("abcde", "", trim: true)
["a", "b", "c", "d", "e"]
時刻偽装
Rubyのtimecopみたいなものを使いたいなと考えていたのですが、ここでJoseさんがMockについて語っているのをみ、てElixirではあまりMockを推奨してなさそうなのでテストの時に時刻ライブラリを差し替える方法を使っています。
defmodule AppName.Support.TimeMock do
use Agent
use Timex
def start_link do
Agent.start_link(fn -> %{is_m: false, m_val: nil} end, name: __MODULE__)
end
def now() do
state = Agent.get(__MODULE__, fn(state) -> state end)
if state[:is_m] do
state[:m_val] |> Timex.from_unix(:microsecond)
else
:os.system_time(:microsecond) |> Timex.from_unix(:microsecond)
end
end
def freeze do
freeze(:os.system_time(:microsecond))
end
def freeze(timestamp) do
Agent.update(__MODULE__, fn(_state) -> %{is_m: true, m_val: timestamp} end)
end
def unfreeze do
Agent.update(__MODULE__, fn(_state) -> %{is_m: false, m_val: nil} end)
end
end
このようなモジュールをテスト時のみ使用するライブラリとして用意しておいて、
下記のようにテスト実行時にライブラリを差し替えるようにしています。テスト用のconfigファイルに上で作成したモジュールを指定する必要があります。
@time_module Application.get_env(:app_name, :time)[:time_module] || Timex
def func() do
epoch = @time_module.now |> Timex.to_unix
# 何らかの処理
end
テスト時には以下のようにして時刻偽装をしています。
describe "get, set" do
let :user, do: Hello.Fixtures.User.create
let :basetime, do: TimeMock.now()
before_all do
TimeMock.freeze() # 時刻を止める
end
after_all do
TimeMock.unfreeze() # 時刻を元に戻す
end
context "time change" do
it do
# 1秒前に進める
Timex.shift(basetime(), seconds: 1)
|> DateTime.to_unix(:microsecond)
|> TimeMock.freeze
# 何らかのテスト...
end
end
end
ライブラリ
Mix.taskのaliases
複数のタスクをまとめられます。
defp aliases do
["ecto.setup": ["ecto.create", "ecto.migrate"]]
end
Ectoで発行するクエリの確認
どんなSQLになるのか確認したい場合に使っています。
query = from(u in User, where: u.user_id == ^user_id, update: [inc: ref_cnt: 10])
Ecto.Adapters.SQL.to_sql(:update_all, repo, query)
# {"UPDATE `users` AS u0 SET u0.`ref_cnt` = u0.`ref_cnt` + 10 WHERE (u0.`user_id` = ?)", [100, 10]}
感想
Elixirはもともと文法がシンプルでコードが読みやすいところが良いと思ってました。
今回仕事で初めて使うことになって長く触ってるうちに、文法はシンプルながらも便利な記法が用意されていて、かつライブラリもまぁまぁ困らないくらいにはあるので非常に実用的な言語だと感じてます。