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: []
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
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
end
See @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
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
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
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