lib/witchcraft/setoid.ex

import TypeClass

defclass Witchcraft.Setoid do
  @moduledoc ~S"""
  A setoid is a type with an equivalence relation.

  This is most useful when equivalence of some data is not the same as equality.

  Since some types have differing concepts of equality, this allows overriding
  the behaviour from `Kernel.==/2`. To get the Setoid `==` operator override,
  simply `use Witchcraft.Setoid`.

  ## Type Class

  An instance of `Witchcraft.Setoid` must define `Witchcraft.Setoid.equivalent?/2`

      Setoid [equivalent?/2]
  """

  alias __MODULE__

  use Witchcraft.Internal, overrides: [==: 2, !=: 2]

  @type t :: any()

  where do
    @doc ~S"""
    Compare two setoids and determine if they are equivalent.

    Aliased as `==`.

    ## Examples

        iex> equivalent?(1, 2)
        false

        iex> import Kernel, except: [==: 2, !=: 2]
        ...> %{a: 1} == %{a: 1, b: 2}
        false

        equivalent?(%Maybe.Just{just: 42}, %Maybe.Nothing{})
        #=> false

    ### Equivalence not equality

        baby_harry = %Wizard{name: "Harry Potter", age: 10}
        old_harry  = %Wizard{name: "Harry Potter", age: 17}

        def chosen_one?(some_wizard), do: equivalent?(baby_harry, some_wizard)

        chosen_one?(old_harry)
        #=> true

    """
    @spec equivalent?(Setoid.t(), Setoid.t()) :: boolean()
    def equivalent?(a, b)
  end

  defalias a == b, as: :equivalent?

  @doc """
  The opposite of `equivalent?/2`.

  ## Examples

      iex> nonequivalent?(1, 2)
      true

  """
  @spec nonequivalent?(Setoid.t(), Setoid.t()) :: boolean()
  def nonequivalent?(a, b), do: not equivalent?(a, b)

  defalias a != b, as: :nonequivalent?

  properties do
    def reflexivity(data) do
      a = generate(data)
      Setoid.equivalent?(a, a)
    end

    def symmetry(data) do
      a = generate(data)
      b = generate(data)

      equal?(Setoid.equivalent?(a, b), Setoid.equivalent?(b, a))
    end

    def transitivity(data) do
      a = b = c = generate(data)
      Setoid.equivalent?(a, b) and Setoid.equivalent?(b, c) and Setoid.equivalent?(a, c)
    end
  end
end

definst Witchcraft.Setoid, for: Integer do
  def equivalent?(int, num), do: Kernel.==(int, num)
end

definst Witchcraft.Setoid, for: Float do
  def equivalent?(float, num), do: Kernel.==(float, num)
end

definst Witchcraft.Setoid, for: BitString do
  def equivalent?(string_a, string_b), do: Kernel.==(string_a, string_b)
end

definst Witchcraft.Setoid, for: Tuple do
  def equivalent?(tuple_a, tuple_b), do: Kernel.==(tuple_a, tuple_b)
end

definst Witchcraft.Setoid, for: List do
  def equivalent?(list_a, list_b), do: Kernel.==(list_a, list_b)
end

definst Witchcraft.Setoid, for: Map do
  def equivalent?(map_a, map_b), do: Kernel.==(map_a, map_b)
end

definst Witchcraft.Setoid, for: MapSet do
  def equivalent?(a, b), do: MapSet.equal?(a, b)
end