yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

Elixir School 3日目

はじめに

  • 前回までにElixir Schoolの基礎編はやりきったので、今回から応用編に取り組んでいきます

Erlangとの相互運用

  • ElixirはErlangVM上で動作するプログラミング言語
  • なので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で定義してあげて、ビヘイビアのモジュール内で定義している関数を呼び出してるモジュール側で実装してあげる。それだけ。