makoto-developer's テックブログ

Elixir ビヘイビアとプロトコル

プロトコル (Protocol) と ビヘイビア (Behaviour) はどちらも多態性を実現する仕組みだが、目的が異なる。

プロトコル — データ型に基づくディスパッチ

異なるデータ型に対して同じ関数を呼び出せるようにする仕組み。外部から型に機能を追加できる。いわゆるアドホック多態性。

elixirdefprotocol Stringify do
  def to_str(data)
end

defimpl Stringify, for: Integer do
  def to_str(n), do: "整数: #{n}"
end

defimpl Stringify, for: BitString do
  def to_str(s), do: "文字列: #{s}"
end

Stringify.to_str(42)      #=> "整数: 42"
Stringify.to_str("hello") #=> "文字列: hello"

ポイント
渡されたデータの型によって呼ばれる実装が変わる。

ビヘイビア — モジュールが満たすべき契約(インターフェース)

モジュールが特定の関数を実装することを強制する仕組み。OOPのインターフェース/抽象クラスに近い。

elixirdefmodule Parser do
  @callback parse(String.t()) :: {:ok, any()} | {:error, String.t()}
end

defmodule JsonParser do
  @behaviour Parser

  @impl true
  def parse(str), do: Jason.decode(str)
end

defmodule CsvParser do
  @behaviour Parser

  @impl true
  def parse(str), do: {:ok, String.split(str, ",")}
end

ポイント
どのモジュールを使うかは呼び出し側が明示的に選ぶ。コンパイル時に未実装の関数があれば警告が出る。

まとめ

プロトコル ビヘイビア
ディスパッチ基準 データの型(自動) モジュール(明示的)
類似概念 Haskellの型クラス, Rustのtrait Java/Goのインターフェース
主な用途 異なる型への統一的な操作 モジュールの契約・差し替え可能な実装
チェック時期 実行時 コンパイル時(警告)

TypeScriptと比べる

プロトコル ≒ 引数の型によるオーバーロード/分岐

// Elixirのプロトコルに相当するイメージ
function toStr(data: number): string;
function toStr(data: string): string;
function toStr(data: unknown): string {
  if (typeof data === "number") return `整数: ${data}`;
  if (typeof data === "string") return `文字列: ${data}`;
  throw new Error("未対応の型");
}

toStr(42);      // "整数: 42"
toStr("hello"); // "文字列: hello"

渡したデータの型で挙動が変わる。しかもElixirでは後から新しい型への実装を追加できる(オープン)。

ビヘイビア ≒ インターフェース (interface)

// Elixirのビヘイビアに相当
interface Parser {
  parse(str: string): { ok: boolean; data?: any; error?: string };
}

class JsonParser implements Parser {
  parse(str: string) {
    return { ok: true, data: JSON.parse(str) };
  }
}

class CsvParser implements Parser {
  parse(str: string) {
    return { ok: true, data: str.split(",") };
  }
}

// どの実装を使うかは呼び出し側が選ぶ

function run(parser: Parser, input: string) {
  return parser.parse(input);
}

モジュール(クラス)が契約を満たしているかをコンパイル時にチェックする仕組み。

Elixir TypeScript相当
プロトコル 引数の型で分岐するオーバーロード(+後から拡張可能)
ビヘイビア interface + implements