Elixir入門 20: 型の仕様とビヘイビア
Tags: elixir, webdev, tutorial, programming
Published: 2019-02-05 01:31:57

---
title: Elixir入門 20: 型の仕様とビヘイビア
published: true
description: Elixir公式サイトの許諾を得て「Typespecs and behaviours」の解説にもとづき、加筆補正を加えて、Elixirにおける型の仕様とビヘイビアの構文および使い方についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Typespecs and behaviours](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html)」の解説にもとづき、加筆補正を加えて、Elixirにおける型の仕様とビヘイビアの構文および使い方についてご説明します。

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

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


## 関数の仕様
デフォルトでは、Elixirは`integer`や`pid`などの基本型とより複雑な型を備えています。たとえば、[`round/1`関数](https://hexdocs.pm/elixir/Kernel.html#round/1)は数値を丸めて整数にして返します。引数が`number`(`integer`または`float`)で戻り値は`integer`です。ドキュメントに型シグネチャはつぎのように表されています。

```elixir
round(number) :: integer
```

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

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

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

```elixir
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
```

```elixir
iex> Example.square_sum(1, 4)
30
```

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


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

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

```elixir
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
```

```elixir
iex> Example.square_sum(%SerialNum{first: 1, last: 4})
30
```

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

```elixir
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
```

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

```elixir
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](http://www.erlang.org/doc/man/dialyzer.html)は、コードの静的な分析をするツールです。したがって、プライベートな関数にも仕様を書く意味はあります。



# ビヘイビア
多くのモジュールは同じAPIを共有しています。たとえば[`Plug`](https://github.com/elixir-lang/plug)が定めるのは、webアプリケーションの中で組み立てられるモジュールの仕様です。各`Plug`はモジュールとしてつくられ、`init/1`と`call/2`の少なくともふたつのパブリックな関数を実装しなければなりません。

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

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

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


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

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

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


## ビヘイビアを受け入れる
ビヘイビアを受け入れるには、モジュールに[`@behaviour`](https://hexdocs.pm/elixir/Module.html#module-behaviour-notice-the-british-spelling)でビヘイビアを指定します。

```elixir
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`の関数をすべて実装していないと、コンパイル時に警告が示されます。

```elixir
defmodule JSONParser do
  @behaviour Parser
  def parse(str), do: "" # ... parse JSON
  # def extensions, do: ["json"]
end
```

```terminal
warning: function extensions/0 required by behaviour Parser is not implemented (in module JSONParser)
```

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

```elixir
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
```

```elixir
iex> Parser.parse!(JSONParser, {:ok, "データ"})
"データ"
```

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


#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- [Elixir入門 07: キーワードリストとマップ](https://dev.to/gumi/elixir-07--39hi)
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- Elixir入門 20: 型の仕様とビヘイビア
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)