Elixir入門 20: 型の仕様とビヘイビア
gumi TECH
Posted on February 5, 2019
本稿はElixir公式サイトの許諾を得て「Typespecs and behaviours」の解説にもとづき、加筆補正を加えて、Elixirにおける型の仕様とビヘイビアの構文および使い方についてご説明します。
型の仕様
Elixirは動的な型づけ言語です。Elixirのすべての型は、実行時に推論されます。それでも、Elixirには型の仕様が含まれています。それは、つぎのふたつを宣言するためです。
- 関数の型シグネチャ(仕様)
- カスタムデータ型
関数の仕様
デフォルトでは、Elixirはinteger
やpid
などの基本型とより複雑な型を備えています。たとえば、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/1
とcall/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入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
番外
Posted on February 5, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.