エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Elixirの演算子多重定義に関する覚書

Elixir には演算子を再定義できる機能が備わっています。

defmodule Foo do
  def lhs + rhs do
    String.to_integer(Integer.to_string(lhs) <> Integer.to_string(rhs))
  end
end

普通の関数と同じように利用することができます。

Foo.+(123, 456)

モジュール名で修飾することなく、一般的な演算子として利用するには import する必要があります。

import Foo, only: [+: 2]

しかしこれだけでは正しく機能しません。

123 + 456
#=> error: function +/2 imported from both Foo and Kernel, call is ambiguous

Kernel.+/2 も同時に定義されているために、どちらの演算子を利用したらよいか判別がつかないからです。

再定義する演算子の引数の型を限定すれば、型を元に判別ができるのではと思うのですが、

defmodule Foo do
  defstruct [:value]

  def new(value \\ 0) do
    %__MODULE__{value: value}
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs.value, rhs.value))
  end
end

残念ながら結果は同じです。

import Foo, only: [+: 2]
Foo.new(123) + Foo.new(456)
#=> error: function +/2 imported from both Foo and Kernel, call is ambiguous

このようなばあい、衝突している関数の import を解除することで解消することができます。

import Kernel, except: [+: 2]
import Foo, only: [+: 2]
Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

ただし、当然ですが、解除した関数を利用することができなくなります。

123 + 456
#=> ** (FunctionClauseError) no function clause matching in Foo.+/2    

これを解消する方法として、再定義した演算子を適用しない「その他のケース」のばあいに元の関数を呼び出せばよさそうです。

defmodule Foo do
  defstruct [:value]

  def new(value \\ 0) do
    %__MODULE__{value: value}
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs.value, rhs.value))
  end

  def lhs + rhs when is_struct(lhs, Foo) and is_number(rhs) do
    new(Kernel.+(lhs.value, rhs))
  end

  def lhs + rhs when is_number(lhs) and is_struct(rhs, Foo) do
    new(Kernel.+(lhs, rhs.value))
  end

  # その他のケース
  def lhs + rhs do
    Kernel.+(lhs, rhs)
  end
end
import Kernel, except: [+: 2]
import Foo, only: [+: 2]

Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

Foo.new(123) + 456
#=> %Foo{value: 579}

123 + Foo.new(456)
#=> %Foo{value: 579}

123 + 456
#=> 579

最後に。 import Kernelimport Foo はいつも組で利用するので、一つの記述で済ませられるようにマクロを定義すると便利です。

defmodule Foo do
  defstruct [:value]

  defmacro __using__(_) do
    quote do
      import Kernel, except: [+: 2]
      import Foo, only: [+: 2]
    end
  end

  # 略
end

これで use するだけで利用できるようになりました。

use Foo

Foo.new(123) + Foo.new(456)
#=> %Foo{value: 579}

123 + 456
#=> 579