🦊

【Elixir】behaviourとUseの考え方

2023/11/25に公開

Elixirのbehaviourを理解するのに手こずった

Elixirにはモジュールを使うユーザーに任意の実装を強制できるbehaviourというしくみがある。
これを理解するのに結構手こずったので、備忘録がてら自分の中での理解を書いていきたい。

useは関係ない

関係ない…は言い過ぎかもしれないけど、useがなくてもbehaviour自体は成立する。ただ、behaviourと相性が良いので合わせて使われるケースが多い。これはまた後で解説する。

なぜbehaviourが必要なのか

Bhaviorのしくみを理解するにはbehaviourが必要な理由から考えると分かりやすい。

たとえばこんな感じのモジュールがあるとする。

defmodule Person do
  def greet(name) do
  IO.puts("Hi! #{name}")
  end
end

Elixirではモジュールを別のモジュールから呼び出すことが可能で、その際呼び出したモジュールが持っている関数を呼び出すことができる。

# iex
iex(1)> Person.greet "Your name"
> Hi! Your name

さて、今Person.greetを呼び出したけれど、これを行うには呼び出したい関数が存在している事を確認する必要がある
もし該当の関数がなかった場合はもちろんエラーが発生する。

ここまで対象のモジュールをiexでインタラクティブに呼び出していた。しかし実際はプログラム中で呼び出される事が多い。
先述したように、対象のモジュールに呼び出したい関数が実装されている事を保証する何らかのしくみがないと、プログラムを実行中に思わぬエラーが発生するかもしれない。

それを防ぐためのしくみがまさにbehaviourの目的と言える。

behaviourとダックタイピング

なぜこのようなことが起きてしまうのだろうか。それはElixirが動的型付き言語だから

動的型付き言語では、オブジェクト自身が型を持っているため関数などで与えられた値に対して無条件でメソッドを発動できる。

function show_length(obj){
  console.log(obj.length)
}

この関数は文字列とリストが来たときはlengthの値を表示できるが、数値が与えられた際にはエラーが発生する。
これを避けるためにはどうすれば良いだろうか。そう、lengthが必ず実装されている事をユーザーに強制させれば良い

このように、動的型付き言語特有の、オブジェクトの振る舞いに注目して処理を実装していくプログラミングスタイルをダック・タイピングと呼ぶ。

名前の由来は「アヒルのように鳴き、アヒルのように歩けばそれはアヒルである」という文言から取られている。
その対象が対象と完全に一致している保証がなくとも、こちらが期待する対象の挙動を振る舞っていれば、それは対象と同一であると仮定して扱う考え方であることがよく分かる。

behaviourを定義する

というわけでElixirを使い、想定される振る舞いを記述してみる。
@callbackというキーワードを使い、このモジュールを使用するユーザーに実装してほしい振る舞いを定義していく。

defmodule Animal do
  @callback cry() :: String.t()
  @callback name() :: String.t()
end

behaviourが定義されたモジュールを使う

上で定義したbehaviourを実際に使ってみる。
behaviourによって定義された関数を実装する際には特に何も付けなくてかまわない。

defmodule Dog do
  @behaviour Animal

  def cry(), do: "bow wow"
  def name(), do: "Dog"
end

behaviourを実装したモジュールを使う

Animalを実装したモジュールの配列を走査し、cry関数をすべて実行するモジュールの例。

defmodule CryAllAnimal do
  def cryall(animals) do

    for animal <- animals, do: IO.puts(animal.cry)
    # bow wow
    # meow
    # [:ok, :ok]
  end
end

Useとの関係

useの動作はとてもシンプルで、

対象のモジュールに定義された__using__という名前のマクロを展開する

以上の処理を行う。

文面にするととても短く見えるが、すべてのモジュールにおいて一貫したマクロ呼び出しが可能なため、非常に強力な機能となっている。

以下はuseしたモジュールに対してgreet関数を展開している。

defmodule UseSample do
  defmacro __using__(_opts) do
    quote do
      def greet(), do: IO.puts("Hey! This is UseSample macro!")
    end
  end
end

defmodule Sample do
  use UseSample

  def sample do
    IO.puts("This is Sample module!")
  end
end

iexでSample.greetと実行するとUseSample__using__に定義してあるgreet()関数が実行される。

BehaviourとUseを併用してみる

先ほどのサンプルコードをuseを使う形に書き直してみる。

# behaviourの定義
defmodule Animal do
  defmacro __using__(_opts) do
    quote do
      @behaviour Animal
    end
  end

  @callback cry() :: String.t()
  @callback name() :: String.t()
end

# behaviourを実装する。従来の記法と違って`use`を使っている。
defmodule Dog do
  use Animal

  @impl Animal
  def cry(), do: "bow wow"

  @impl Animal
  def name(), do: "Dog"
end

このようにuseを併用することで、今までbehaviourを記述するたびに書いていた@behaviourという文言を省略できるようになった。
また、useの実態はマクロなので任意の処理をbehaviourと合わせて展開する事もできる。

実際の使用例

Plug

Elixirにおいて最もポピュラーなbehaviourはPlugではないだろうか。

簡単に説明すると、Plugとは必要な関数を実装するだけでWebサーバを構築できるしくみ。
これを使うことでシンプルにWebサーバを実装できる。

ここでは先の説明を踏まえてcowboy_plugを使って簡単なWebサーバを構築してみる。

まず始めにMixプロジェクトを作成する。

mix new sample

以上で環境構築は完了したので、ここからは実際にWebサーバを実装していく。

defmodule Sample.Router do
    use Plug.Router

    plug :match
    plug :dispatch

    get "/" do
        send_resp(conn, 200, "Hello world!")
    end

    match _ do
        send_resp(conn, 404, "not found\n")
    end
end

iex -S mixでiexを起動して以下のスクリプトを入力する。

{:ok, _} = Plug.Cowboy.http Sample.Router, []

エラーがなければ{:ok, #PID<0.268.0>}のように表示される。もちろんPIDの値は実行するたびに変わる。

ブラウザからlocalhost:4000にアクセスして上のように表示されたら成功。
またlocalhost:4000/greetにアクセスするとHowdy!と表示される。

まとめ

  • useとbehaviourは直接は関係ない。が、併用すると便利。
  • behaviourは振る舞いを定義する
  • behaviourはElixirにおいて非常に多用される
  • Plugを使うことで簡単にWebサーバを実装する事ができる。
GitHubで編集を提案

Discussion