Hexパッケージ
Mix.install([
{:jason, "~> 1.4"},
{:croma, "~> 0.11.3"},
{:httpoison, "~> 2.1"},
{:meck, "~> 0.9.2"}
])
Hexパッケージ
ElixirにはHexというパッケージマネージャーがある。
Elixirではmix
というツールでプロジェクト管理をする。
-
このように
mix.exs
というプロジェクト定義ファイルに依存パッケージを記述する
Erlang向けのHexパッケージもあり、Elixirでも問題なく使用可能。
ここではいくつかのHexパッケージを紹介する。
- Jason: 高速なJSONパーサー・ジェネレーター
- Croma: Antikythera創始者の方が作成した、型ベースプログラミングのためのマクロ集
- meck: Erlang向けのモッキングライブラリ
なお、AntikytheraもHexパッケージとして公開されている。が、ここでは扱わない。
Livebook上での使用
動的にパッケージのインストールを行うため、Mix.install/2
を利用する。
Livebookではインストール専用のセルがノートブック上部に配置されているため、そこに必要なパッケージを追加してsetupボタンを押す。
Jason
-
Jason.decode/1
でJSON形式の文字列をElixirの値にパースする-
Jason.decode!/1
はパースに失敗するとエラー
-
-
Jason.encode/1
でElixirの値をJSON文字列にエンコードする-
Jason.encode!/1
はエンコードに失敗するとエラー
-
WebサーバーではHTTPリクエストやレスポンスのbodyに対して適用することが多い。
json = """
{
"x": 0,
"y": "a",
"array": [0, 1, 2],
"nested": {
"inner": {}
}
}
"""
{:ok, map} = Jason.decode(json)
map
File.read!("./resources/7/test.json")
|> Jason.decode!()
{:error, reason} = Jason.decode("{[]}")
result = Jason.decode!("{[]}")
# => Jason.DecodeError
{:ok, json} = Jason.encode(%{a: 0})
json
Croma
- Elixirで型ベースプログラミングを行うのを楽にするマクロ集
バリデーションつきStructの定義
# 例1. フィールドのバリデーションつきStructを定義
# new/1 や new!/1 関数でStructを生成する時、フィールドのvalidationが自動で行われる
defmodule TestStruct do
# xは整数でなければならない
use Croma.Struct,
recursive_new?: true,
fields: [
x: Croma.Integer
]
end
defmodule OtherStruct do
# xは整数でなければならない
use Croma.Struct,
recursive_new?: true,
fields: [
x: Croma.Integer
]
end
TestStruct.new(%{x: 0})
# xのvalueが整数ではないので失敗
TestStruct.new(%{x: 1.0}
# Structの種類を考慮したパターンマッチ
%TestStruct{x: x} = TestStruct.new!(%{x: 0})
x
# Cromaの使用に関わらず、異なるStructどうしはマッチしない
%OtherStruct{x: x} = TestStruct.new!(%{x: 0})
# => MatchError
# Structのフィールドをより詳細に定義する例
defmodule Food do
# 特定のatomだけ許可
defmodule Category do
use Croma.SubtypeOfAtom, values: [:meat, :vegitable, :fruit]
end
# 正規表現で長さ1~50の文字列を許可
defmodule Name do
use Croma.SubtypeOfString, pattern: ~r/\A.{1,50}\z/
end
use Croma.Struct,
recursive_new?: true,
fields: [
category: Category,
name: Name
]
end
defmodule Eater do
# Food struct を引数に取る関数
def eat(%Food{category: category, name: name}) do
case category do
:vegitable -> "I do not like #{name}, but I eat it..."
_ -> "I love #{name}!"
end
end
end
[
%{category: :meat, name: "pork"},
%{category: :vegitable, name: "tomato"},
%{category: :fruit, name: "apple"}
]
|> Enum.map(&Food.new!/1)
|> Enum.map(&Eater.eat/1)
# nameの文字数が50より大きい場合にはエラー
Food.new(%{
category: :meat,
name: "this meat is something having too long name and we cannot pronounce it"
})
# nameの文字数が0の場合にエラーになることを確かめよう
Food.new(%{})
# categoryに未定義のatomが渡される場合もエラーになることを確かめよう
Food.new(%{})
関数定義における利用例
こちらは参考までに。
関数定義時に関数の型スペックを簡潔に表したり、引数や返り値が期待した型であることのvalidationをしたりできる。
-
def
に代わるdefun
-
defp
に代わるdefunp
defmodule CromaTestModule do
# Cromaが提供するマクロを利用するために必要
use Croma
@moduledoc """
`defun`や`defunp`で引数に続けて`:: type`のように型を書く。
`v[]`で型を囲むと、ランタイム時に引数の型が仕様にあっていることのvalidationが行われる。
返り値も同様に表現する。
`defun`や`defunp`で多重定義する際は、無名関数の多重定義のような書き方をする必要がある。
"""
# Elixir標準の記法
@spec add(integer, integer) :: integer
def add(x, y) do
x + y
end
# Cromaを使った記法(型バリデーション付き)
defun add_integers(x :: v[integer], y :: v[integer]) :: v[integer] do
x + y
end
# Cromaを使った記法(型スペックのみ)
defun add_integers_without_validation(x :: integer, y :: integer) :: integer do
x + y
end
defun accept_hello_atom(value :: atom) :: Croma.Result.t(:hello, String.t()) do
:hello -> {:ok, :hello}
other_atom -> {:error, format_message(other_atom)}
end
defunp format_message(value :: v[atom]) :: v[String.t()] do
"#{value} is not :hello atom"
end
end
CromaTestModule.add_integers(0, 1)
CromaTestModule.add_integers(0.0, 1)
# => %RuntimeError{message: "validation error: x is not a valid integer"}
CromaTestModule.add_integers_without_validation(0.0, 1)
CromaTestModule.accept_hello_atom(:hello)
CromaTestModule.accept_hello_atom(:world)
meck
Erlangのモッキングライブラリ。
モジュールの関数の振る舞いを動的に変えたり、引数のテストを行ったり、呼び出し回数を計測したりできる。
テストコードでよく使用される。
例えば外部サービスに依存する関数・副作用のある関数のテストを行いたいとき、モックを使用したくなる。
-
後の
HttpBin.send_request/3
を使用し、リクエスト先のサーバーがダウンしている状況のテストをしたい -
外部サービスがダウンしている時(HTTP status 500が返るとする)の処理が期待どおりか確かめたい
- しかし、外部サービスを実際にダウンさせることはできない
- そもそもテストで、外部サービスにリクエストを送りたくない
モックを使うと, 関数呼び出しを監視することができ、必要ならば同じインターフェースで異なる振る舞いをする別の関数に差し替えることができる。
つまり、下の図のようにあるテストケースのときだけHttpBin.send_request/3
を常に%{status: 500}
を返す別の関数に差し替えることができる。
defmodule HttpBin do
# HTTPリクエストを行うための事前準備
HTTPoison.start()
@endpoint "https://httpbin.org"
def send_request(method, body_map, header_map) do
url = @endpoint <> "/#{method}"
case HTTPoison.post!(url, Jason.encode!(body_map), header_map) do
%HTTPoison.Response{body: res_body, status_code: 200} ->
%{status: 200, body: Jason.decode!(res_body)}
%HTTPoison.Response{status_code: 500} ->
%{status: 500}
end
end
end
HttpBin.send_request(:post, %{foo: "bar"}, %{"Content-Type" => "application/json"})
meckモジュールを使ってHttp.send_request/3
が常に%{status: 500}
を返すよう振る舞いを変える。
# Erlangのモジュール名は、atomで表す決まりになっている => :meck
# モックするための事前準備
:meck.new(HttpBin, [:passthrough])
# HttpBin.send_request/3 を任意の関数に差し替える
:meck.expect(
# 対象モジュール
HttpBin,
# 関数名のatom
:send_request,
# 関数と同じarityを持つ無名関数
fn _method, _body, _header -> %{status: 500} end
)
HttpBin.send_request(:post, %{foo: "bar"}, %{"Content-Type" => "application/json"})
Http.send_request/3
の振る舞いが無名関数fn _method, _body, _header -> %{status: 500} end
に差し替わったようだ!
本当にそうなのか、さらに確かめてみよう。
:meck.expect(HttpBin, :send_request, fn method, _body, _header ->
IO.puts("Mocked function is called 😁")
IO.puts("Tried to request by #{method} method")
%{status: 500}
end)
HttpBin.send_request(:post, %{foo: "bar"}, %{"Content-Type" => "application/json"})
😁
モックは他の副作用、例えば日時や乱数が関わるようなテストを実施する際にもよく使われる。
# モックしたモジュールを削除
:meck.unload(HttpBin)