TypeClass (TypeClass v1.2.8) 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: []
endInternal 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
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.
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
endSee @force_type_class section in the README for more.
Convenience alises for definst/3
  
  1. Implicit :for
Shortcut for
definst ATypeClass, for: __MODULE__ do
  # required function definitions
endwhen 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
  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
endSee @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.NameEither 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
  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