TypeClass v1.0.0 TypeClass

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

Summary

Macros

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

Variant of definst/2 for use inside of a defstruct module definition

Define an instance of the type class. The rough equivalent of defimpl. defimpl 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

Macros

conforms(list)

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

conforms(datatype, list)

Check that a datatype conforms to the class hierarchy and properties

defalias(fun_head, list)

Delegate to a local function

defclass(class_name, list)

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

Examples

defclass Semigroup do
  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
definst(class, list)

Variant of definst/2 for use inside of a defstruct module definition

definst(class, opts, list)

Define an instance of the type class. The rough equivalent of defimpl. defimpl 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
  def concat(a, b), do: a ++ b
end
properties(list)

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
where(list)

Describe functions to be instantiated. Creates an internal protocol.

Examples

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

  # ...
end