Elixir School 3日目
はじめに
- 前回までにElixir Schoolの基礎編はやりきったので、今回から応用編に取り組んでいきます
Erlangとの相互運用
- ElixirはErlangのVM上で動作するプログラミング言語
- なのでErlangのライブラリなどをElixirから利用することができる
- Erlangモジュールは
:os
や:timer
のように小文字のアトムで表す
defmodule Example do def timed(fun, args) do # Erlang側のtimerというモジュールを呼び出す {time, result} = :timer.tc(fun, args) IO.puts("Time: #{time} μs") IO.puts("Result: #{result}") end end
ちなみにiex上で呼び出すことももちろん可能
iex(3)> :timer.minutes(1) 60000 iex(4)> :timer.seconds(1) 1000
この場合は1秒と1分がmsになって返却されていることがわかる。 Erlangを書く予定は今の所ないので、必要になったらそのタイミングで違いとかは勉強しようと思う。
例外ハンドリング
- まず、
raise
でエラーを起こしてみる
# raise/1であればRuntimeErrorが発生する iex(5)> raise "error!!!!!!" ** (RuntimeError) error!!!!!! # raise/2であれば指定したErrorを発生させられる iex(5)> raise ArgumentError, "another error occured" ** (ArgumentError) another error occured
- ハンドリング自体は
try/rescue
で行う
iex(2)> try do ...(2)> raise "oh no" ...(2)> rescue ...(2)> e in RuntimeError -> IO.puts(e.message) ...(2)> end oh no :ok
- Javaのfinally句的なものとして
after
というものがある(明示的にコネクションを切るとか、ファイルを閉じるとか、そいうこ処理をこの中に埋め込む)
iex> try do ...> raise "Oh no!" ...> rescue ...> e in RuntimeError -> IO.puts("An error occurred: " <> e.message) ...> after ...> IO.puts "The end!" ...> end
- 独自例外の定義も可能
defmodule ExampleError do defexception message: "an example error has occurred" end
仕様と型
@spec
について
@spec
アノテーションと静的分析ツールを使うことで、関数が予期せぬ型を返すみたいなことを防ぐことができる- 手順としては下記の通り
まず、分析したい関数に対して @spec
に続いて関数名、引数、戻り値を定義する
@spec sum_product(integer) :: integer def sum_product(a) do [1, 2, 3] |> Enum.map(fn el -> el * a end) |> Enum.sum() end
そして、プロジェクト配下の mix.exs
の中でdialyxir
を depsに追加してあげる参考
defp deps do [ {:dialyxir, "~> 1.0"} ] end
$ mix deps.get && mix deps.compile $ mix dialyzer
上記の例だと、Enum.sun()の戻り値はnumberですが関数の戻り値としてはintegerを期待してるので、落ちると思ったのですがそこはよしなってくれるのか落ちませんでした。
ちなみに、これを String
とかにすると下記の通り怒られます。
lib/elixir_school.ex:11:invalid_contract The @spec for the function does not match the success typing of the function. Function: ElixirSchool.sum_product/1 Success typing: @spec sum_product(_) :: number()
- 結構便利だなと思った一方で、どこまでやるのがベストプラクティスなんだろうなと疑問は生じたというのが、個人的な所感
@type
について
- 独自の型を定義した際に、それを関数の引数にする際に構造体が複雑であると可読性が下がることがある。
- それを解決するのが
@type
である
例えば下記のようなコードがあったとする
@spec sum_times(integer, %Examples{first: integer, last: integer}) :: integer def sum_times(a, params) do for i <- params.first..params.last do i end |> Enum.map(fn el -> el * a end) |> Enum.sum() |> round end
- この場合、Exampleモジュールの中で定義された構造体は、他の関数でも使いたいということが生じた場合に毎回書かないといけなくなります。
- また、それぞれで定義することで、構造体が変更されたさいに呼び出してるところで全て修正しないといけないので、保守性がひくくなってしまいます。
- そこで、
@type
の出番がくる
defmodule Examples do defstruct first: nil, last: nil @type t(first, last) :: %Examples{first: first, last: last} @type t :: %Examples{first: integer, last: integer} end
呼び出しもとでは、下記のように修正できる
@spec sum_times(integer, Examples.t()) :: integer def sum_times(a, params) do for i <- params.first..params.last do i end |> Enum.map(fn el -> el * a end) |> Enum.sum() |> round end
こうすることで、関数で @spec
で定義していても、構造体に変更するだけで他の修正は不要になる。
ビヘイビア
defmodule Example.Worker do @callback init(state :: term) :: {:ok, new_state :: term} | {:error, reason :: term} @callback perform(args :: term, state :: term) :: {:ok, result :: term, new_state :: term} | {:error, reason :: term, new_state :: term} end
- 上記のビヘイビアを実装クラスから呼び出す
defmodule Example.Downloader do @behaviour Example.Worker def init(opts), do: {:ok, opts} def perform(url, opts) do url |> HTTPoison.get!() |> Map.fetch(:body) |> write_file(opts[:path]) |> respond(opts) end defp write_file(:error, _), do: {:error, :missing_body} defp write_file({:ok, contents}, path) do path |> Path.expand() |> File.write(contents) end defp respond(:ok, opts), do: {:ok, opts[:path], opts} defp respond({:error, reason}, opts), do: {:error, reason, opts} end
- 使い方としては、Javaのインターフェースとほぼ同じ。
- ビヘイビアとして呼び出したいモジュールを
@behaviour
で定義してあげて、ビヘイビアのモジュール内で定義している関数を呼び出してるモジュール側で実装してあげる。それだけ。