loading...
gumi TECH Blog

Elixir入門 20: 型の仕様とビヘイビア

gumitech profile image gumi TECH ・3 min read

本稿はElixir公式サイトの許諾を得て「Typespecs and behaviours」の解説にもとづき、加筆補正を加えて、Elixirにおける型の仕様とビヘイビアの構文および使い方についてご説明します。

型の仕様

Elixirは動的な型づけ言語です。Elixirのすべての型は、実行時に推論されます。それでも、Elixirには型の仕様が含まれています。それは、つぎのふたつを宣言するためです。

  • 関数の型シグネチャ(仕様)
  • カスタムデータ型

関数の仕様

デフォルトでは、Elixirはintegerpidなどの基本型とより複雑な型を備えています。たとえば、round/1関数は数値を丸めて整数にして返します。引数がnumber(integerまたはfloat)で戻り値はintegerです。ドキュメントに型シグネチャはつぎのように表されています。

round(number) :: integer

::は左の関数が右に示した型の値を返すという意味です。関数の仕様は定義の直前に@specディレクティブを添えて書きます。たとえば、round/1関数であれば、つぎのとおりです。

@spec round(number) :: integer
def round(number), do: # 実装...

つぎの例は、モジュールに定めた関数に仕様を定めています。関数にふたつの整数を渡すと、連番整数の平方和が返されます。最後にround/1で丸めているのは、戻り値を整数(integer)にするためです。

defmodule Example do
  @spec square_sum(integer, integer) :: integer
  def square_sum(first, last) do
    for i <- first..last do
      i
    end
    |> Enum.map(fn num -> num * num end)
    |> Enum.sum()
    |> round
  end
end
iex> Example.square_sum(1, 4)
30

Elixirは複合型もサポートします。たとえば、整数のリストは[integer]です。Elixirにどのような型が組み込まれているのかについては「Typespecs」をご覧ください。

カスタム型の宣言

Elixirには多くの使いやすい型が組み込まれています。さらに、必要に応じて定められるのがカスタム型です。モジュールに@typeディレクティブで定義します。

つぎの例は、関数の引数を構造体とし、関数には仕様が添えられています。この構造体にカスタム型を宣言しましょう。

defmodule SerialNum do
  defstruct first: nil, last: nil
end

defmodule Example do
  @spec square_sum(%SerialNum{first: integer, last: integer}) :: integer
  def square_sum(nums) do
    for i <- nums.first..nums.last do
      i
    end
    |> Enum.map(fn num -> num * num end)
    |> Enum.sum()
    |> round
  end
end
iex> Example.square_sum(%SerialNum{first: 1, last: 4})
30

@typeディレクティブのあとに構造体の型名、かっこ()内にはフィールドの型が変数で与えられます。その型を決めるのは呼び出す関数に定めた仕様です。構造体を複数のモジュールや関数で使うとき、仕様の定めが見やすく簡潔になります。

defmodule SerialNum do
  defstruct first: nil, last: nil
  @type t(first, last) :: %SerialNum{first: first, last: last}
end

defmodule Example do
  @spec square_sum(SerialNum.t(integer, integer)) :: integer
  def square_sum(nums) do
    for i <- nums.first..nums.last do
      i
    end
    |> Enum.map(fn num -> num * num end)
    |> Enum.sum()
    |> round
  end
end

構造体の側で型を決めることもできます。つぎの例ではカスタム型を加え、関数の仕様がフィールドの型を与えないとき構造体の宣言した型としています。

defmodule SerialNum do
  defstruct first: nil, last: nil
  @type t(first, last) :: %SerialNum{first: first, last: last}
  @type t :: %SerialNum{first: integer, last: integer}
end

defmodule Example do
  @spec square_sum(SerialNum.t()) :: integer
  def square_sum(nums) do
    for i <- nums.first..nums.last do
      i
    end
    |> Enum.map(fn num -> num * num end)
    |> Enum.sum()
    |> round
  end
end

@typeで宣言したカスタム型はエクスポートされて、他のモジュールから使えます。モジュール内でのみ用いるカスタム型は、@typepディレクティブで宣言してください。

静的コード分析

Elixirは動的言語です。そのため、型についての情報は開発者に役立つものの、コンパイラは見ません。けれど、型の宣言を確かめるツールがあります。たとえば、ErlangのDialyzerは、コードの静的な分析をするツールです。したがって、プライベートな関数にも仕様を書く意味はあります。

ビヘイビア

多くのモジュールは同じAPIを共有しています。たとえばPlugが定めるのは、webアプリケーションの中で組み立てられるモジュールの仕様です。各Plugはモジュールとしてつくられ、init/1call/2の少なくともふたつのパブリックな関数を実装しなければなりません。

ビヘイビアはつぎのふたつの機能を担います。

  • モジュールが実装しなければならない関数を定めます。
  • 必要な関数をモジュールが実装しているかどうか確かめます。

ビヘイビアは、Javaのようなオブジェクト指向言語におけるインタフェースにあたります。モジュールが実装すべき関数シグネチャです。

ビヘイビアを定める

JSONやMessagePackなどの構造化されたデータを解析したいとします。そのためのパーサをそれぞれつくるなら、データの構造は異なっても、必要な基本の機能は共通するでしょう。そのような場合は、実装すべき関数をビヘイビアで定めます。

ビヘイビアを受け入れるモジュールは、@callbackディレクティブが定める関数をすべて実装しなければなりません(@behaviour参照)。構文は@specディレクティブと基本的に同じです。termはElixirの任意の型を表します。文字列の型をString.tとすることについては「Typespecs」の「Notes」をご参照ください。

defmodule Parser do
  @callback parse(String.t) :: {:ok, term} | {:error, String.t}
  @callback extensions() :: [String.t]
end

ビヘイビアを受け入れる

ビヘイビアを受け入れるには、モジュールに@behaviourでビヘイビアを指定します。

defmodule JSONParser do
  @behaviour Parser
  def parse(str), do: "" # ... JSONの解析処理
  def extensions, do: ["json"]
end

defmodule YAMLParser do
  @behaviour Parser
  def parse(str), do: "" # ... YAMLの解析処理
  def extensions, do: ["yml"]
end

モジュールがビヘイビアに定めた@callbackの関数をすべて実装していないと、コンパイル時に警告が示されます。

defmodule JSONParser do
  @behaviour Parser
  def parse(str), do: "" # ... parse JSON
  # def extensions, do: ["json"]
end
warning: function extensions/0 required by behaviour Parser is not implemented (in module JSONParser)

動的呼び出し

ビヘイビアはよく動的呼び出し(dynamic dispatching)とともに用いられます。つぎの例では、ビヘイビアを定めたモジュールに関数が加えられています。この関数が受け取るのは、ビヘイビアを受け入れたモジュールです。

defmodule Parser do
  @callback parse(String.t) :: {:ok, term} | {:error, String.t}
  @callback extensions() :: [String.t]
  def parse!(implementation, contents) do
    case implementation.parse(contents) do
      {:ok, data} -> data
      {:error, error} -> raise ArgumentError, "parsing error: #{error}"
    end
  end
end

defmodule JSONParser do
  @behaviour Parser
  def parse(str), do: str # ... parse JSON
  def extensions, do: ["json"]
end
iex> Parser.parse!(JSONParser, {:ok, "データ"})
"データ"

動的呼び出しをするために、ビヘイビアを定めることが必須ではありません。けれど、このふたつはよくともに用いられます。

Elixir入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide