TypeClass (TypeClass v1.2.11-blazing) View Source

Helpers for defining (bootstrapped, semi-)principled type classes

Generates a few modules and several functions and aliases. There is no need to use these internals directly, as the top-level API will suffice for actual productive use.

Example

defclass Semigroup do
  use Operator

  where do
    @operator :<|>
    def concat(a, b)
  end

  properties do
    def associative(data) do
      a = generate(data)
      b = generate(data)
      c = generate(data)

      left  = a |> Semigroup.concat(b) |> Semigroup.concat(c)
      right = Semigroup.concat(a, Semigroup.concat(b, c))

      left == right
    end
  end
end

definst Semigroup, for: List do
  def concat(a, b), do: a ++ b
end

defclass Monoid do
  extend Semigroup

  where do
    def empty(sample)
  end

  properties do
    def left_identity(data) do
      a = generate(data)
      Semigroup.concat(empty(a), a) == a
    end

    def right_identity(data) do
      a = generate(data)
      Semigroup.concat(a, empty(a)) == a
    end
  end
end

definst Monoid, for: List do
  def empty(_), do: []
end

Internal Structure

A type_class is composed of several parts:

  • Dependencies
  • Protocol
  • Properties

Dependencies

Dependencies are the other type classes that the type class being defined extends. For instance, Monoid has a Semigroup dependency.

It only needs the immediate parents in the chain, as those type classes will have performed all of the checks required for their parents.

Protocol

defclass Foo generates a Foo.Proto submodule that holds all of the functions to be implemented (it's a normal protocol). It's a very lightweight & straightforward, but The Protocol should never need to be called explicitly.

Macro: where do Optional

Properties

Being a (quasi-)principled type class also means having properties. Users must define at least one property, plus at least one sample data generator. These will be run at compile time and refuse to compile if they don't pass.

All custom structs need to implement the TypeClass.Property.Generator protocol. This is called automatically by the prop checker. Base types have been implemented by this library.

Please note that class functions are aliased to the last segment of their name. ex. Foo.Bar.MyClass.quux is automatically usable as MyClass.quux in the proprties block

Macro: properties do Non-optional

Link to this section Summary

Functions

Variant of conforms/2 that can be called within a data module

Check that a datatype conforms to the class hierarchy and properties

Delegate to a local function

Top-level wrapper for all type class modules. Used as a replacement for defmodule.

Convenience alises for definst/3

Define an instance of the type class. The rough equivalent of defimpl. definst will check the properties at compile time, and prevent compilation if the datatype does not conform to the protocol.

Define properties that any instance of the type class must satisfy. They must by unary (takes a data seed), and return a boolean (true if passes).

Describe functions to be instantiated. Creates an internal protocol.

Link to this section Functions

Link to this macro

conforms(opts)

View Source (macro)

Variant of conforms/2 that can be called within a data module

Link to this macro

conforms(datatype, opts)

View Source (macro)

Check that a datatype conforms to the class hierarchy and properties

Link to this macro

defalias(fun_head, list)

View Source (macro)

Delegate to a local function

Link to this macro

defclass(class_name, list)

View Source (macro)

Top-level wrapper for all type class modules. Used as a replacement for defmodule.

Examples

defclass Semigroup do

  # @force_type_class true

  where do
    def concat(a, b)
  end

  properties do
    def associative(data) do
      a = generate(data)
      b = generate(data)
      c = generate(data)

      left  = a |> Semigroup.concat(b) |> Semigroup.concat(c)
      right = Semigroup.concat(a, Semigroup.concat(b, c))

      left == right
    end
  end
end

See @force_type_class section in the README for more.

Link to this macro

definst(class, list)

View Source (macro)

Convenience alises for definst/3

1. Implicit :for

Shortcut for

definst ATypeClass, for: __MODULE__ do
  # required function definitions
end

when implementing type class instances inside the module where the data type is defined.

Examples

defmodule Name do
  import Algae
  import TypeClass
  use Witchcraft

  defdata do
    name :: String.t()
  end

  definst Witchcraft.Functor do
    @force_type_instance true
    def map(%{name: name}, f), do: %{name: f.(name)}
    # def map(_, _), do: 27 # %{name: f.(name)}
  end

  def add_title(%__MODULE__{} = name, title) do
    name ~> &Kernel.<>(title, &1)
  end
end

iex(3)> name = X.new("Kilgore Troutman")
%X{name: "Kilgore Troutman"}

iex(4)> X.add_title(name, "Dr. ")
%{name: "Dr. Kilgore Troutman"}

NOTE: copy-pasting the above in IEx won't work because definst checks properties at compile time.

2. No body

When you only want to check the properties (ex. when there is no where block, such as in Witchcraft.Monad).

Examples

# Dependency
defclass Base do
  where do
    def plus_one(a)
  end

  properties do
    def pass(_), do: true
  end
end

# No `where`
defclass MoreProps do
  extend Base

  properties do
    def yep(a), do: equal?(a, a)
  end
end

definst Base, for: Integer do
  def plus_one(a), do: a + 5
end

definst MoreProps, for: Integer
Link to this macro

definst(class, opts, list)

View Source (macro)

Define an instance of the type class. The rough equivalent of defimpl. definst will check the properties at compile time, and prevent compilation if the datatype does not conform to the protocol.

Examples

definst Semigroup, for: List do

  # @force_type_instance true

  def concat(a, b), do: a ++ b
end

See @force_type_instance section in the README for more.

__MODULE__'s meaning changes inside definst

Beware that the value of __MODULE__ inside definst will be different from the outside context: definst's do block will be invoked inside defimpl macro's body, and defimpl creates its own container module to run things in.

For example, the code below won't compile:

defmodule Name do
  import Algae
  import TypeClass
  use Witchcraft

  defdata do
    name :: String.t()
  end

  definst Witchcraft.Functor, for: __MODULE__ do
    @force_type_instance true

    def map(%__MODULE__{name: name}, f) do
      __MODULE__.new(name)
    end
  end
end

# ** (CompileError) lib/instance/assword.ex:13:
#    Witchcraft.Functor.Proto.Instance.Name.__struct__/0 is undefined,
#    cannot expand struct Witchcraft.Functor.Proto.Instance.Name

Either use the full module name, or alias it, if too long, such as

defmodule Name do
  # (...)
  # here

  definst Witchcraft.Functor, for: __MODULE__ do
    # or here
    # (...)
  end
end
Link to this macro

properties(list)

View Source (macro)

Define properties that any instance of the type class must satisfy. They must by unary (takes a data seed), and return a boolean (true if passes).

generate is automatically imported

Examples

defclass Semigroup do
  # ...

  properties do
    def associative(data) do
      a = generate(data)
      b = generate(data)
      c = generate(data)

      left  = a |> Semigroup.concat(b) |> Semigroup.concat(c)
      right = Semigroup.concat(a, Semigroup.concat(b, c))

      left == right
    end
  end
end

Describe functions to be instantiated. Creates an internal protocol.

Examples

defclass Semigroup do
  where do
    def concat(a, b)
  end

  # ...
end