2.3 Typeclasses
2.3.1 Defining typeclasses and typeclass instances
syntax
(class maybe-superclasses (class-id var-id ...) maybe-fundeps [method-id : method-type maybe-default-method-impl] ... maybe-deriving-transformer)
maybe-superclasses = superclass-constraint ... => |
maybe-fundeps = #:fundeps [fundep-spec ...] |
fundep-spec = [determinant-id ...+ -> dependent-id ...+] maybe-default-method-impl = default-method-impl-expr |
maybe-deriving-transformer = #:deriving-transformer deriving-transformer-expr |
A typeclass defines a set of typeclass methods, each named method-id, which are operations that must be implemented by all members of the class. Implementations of typeclass methods must match the provided method-type, with the var-ids replaced by the types the instance is being defined for.
For each method-id, a default method may be provided as default-method-impl-expr, which will be used if any instance opts to not specify an explicit implementation for that method. Each default method implementation must be polymorphic enough to be a valid implementation for any instance of the class. Default methods are generally used when a typeclass method may be defined in terms of other typeclass methods, but the implementor can be given a choice of which methods to implement, or they can provide a more efficient implementation for commonly-used methods.
Each fundep-spec, if provided, declares a functional dependency between parameters of the class, where each determinant-id and dependent-id must be included in the class’s var-ids. A functional dependency introduces a constraint between instances of the class: each combination of the parameters specified by the determinant-ids must uniquely determine the parameters specified by the dependent-ids. For example, given the following class declaration:
…then each instance of C with the same type for a must also have the same type for c. For example, these instances would be permitted together:
(instance (C Integer Unit String)) (instance (C Integer Bool String))
…but these instances would not:
(instance (C Integer Unit String)) (instance (C Integer Bool Double))
Restriction of this sort is useful mostly because the knowledge that a class will be used this way allows the typechecker to perform better type inference. Without a functional dependency, use of a typeclass-constrained function requires all of its parameters to be known, but with a functional dependency, the typechecker can solve a typeclass constraint when only a subset of the parameters are known.
If deriving-transformer-expr is provided, it is evaluated in the transformer environment to obtain a deriving transformer for the defined class. A deriving transformer must be a syntax transformer, which will be used to expand uses of derive-instance or data #:deriving clauses that reference the class.
syntax
(instance instance-spec [method-id method-expr] ...)
instance-spec = (class-id instance-type ...) |
(forall [var-id ...] maybe-constraints (class-id instance-type ...)) maybe-constraints = instance-constraint ... => |
syntax
(derive-instance class-id . args)
2.3.2 Printing for debugging
2.3.3 Equality
An implementation for at least one of == or /= must be provided. If one is omitted, a default implementation will be provided in terms of the other.
2.3.4 Semigroups and monoids
An associative operation. That is, ++ must obey the following law:Examples:
value
mempty : a
{a ++ mempty} = a {mempty ++ a} = a Examples:
2.3.5 Functors
The map function can be thought of as applying a function to each “element” of some “container”. This metaphor applies to many members of the Functor typeclass, such as List and Maybe, but does not apply well to all of them.
More generally, map can be viewed as a “lifting” operation, which “lifts” a function of type {a -> b} to a function of type {(f a) -> (f b)} for some type f. This is a very general notion, and the meaning of such an operation is highly dependent on the particular choice of f.
All map implementations must obey the following laws:
(map id) = id (map {f . g}) = {(map f) . (map g)}
> {(+ 1) <$> {1 :: 2 :: Nil}}
: (List Integer)
{2 :: 3 :: Nil}
> {(+ 1) <$> (Just 10)}
: (Maybe Integer)
(Just 11)
> {(+ 1) <$> Nothing}
: (Maybe Integer)
Nothing
> {10 <$ (Just 1)}
: (Maybe Integer)
(Just 10)
> {10 <$ {1 :: 2 :: 3 :: Nil}}
: (List Integer)
{10 :: 10 :: 10 :: Nil}
2.3.6 Applicative functors
typeclass
(class (Functor f) => (Applicative f)
[pure : (forall [a] {a -> (f a)})] [<*> : (forall [a b] {(f {a -> b}) -> (f a) -> (f b)})])
Applicative functors must satisfy the following laws:
{(pure id) <*> v} = v {(pure .) <*> u <*> v <*> w} = {u <*> (v <*> w)} {(pure f) <*> (pure x)} = (pure (f x)) {u <*> (pure y)} = {(pure (& y) <*> u)}
As a consequence of these laws, the Functor instance for f will satisfy:
Lifts a value.Examples:
Applies a function in a context. While map/<$> “lifts” a pure function to a function that operates on a functor, <*> applies a function that is already inside the context of a functor.Examples:
> {(Just not) <*> (Just True)}
: (Maybe Bool)
(Just False)
> {(Just not) <*> (Just False)}
: (Maybe Bool)
(Just True)
> {(Just not) <*> Nothing}
: (Maybe Bool)
Nothing
> {(: Nothing (Maybe {Bool -> Bool})) <*> (Just True)}
: (Maybe Bool)
Nothing
Due to currying, this is especially useful in combination with <$> to apply a multi-argument function to multiple arguments within the context of some functor. For example, Maybe implements a sort of short-circuiting, where any Nothing will cause the entire computation to produce Nothing.
Examples:
> {+ <$> (Just 1) <*> (Just 2)}
: (Maybe Integer)
(Just 3)
> {+ <$> Nothing <*> (Just 2)}
: (Maybe Integer)
Nothing
> {+ <$> (Just 1) <*> Nothing}
: (Maybe Integer)
Nothing
This works because {f <$> x} is guaranteed to be equivalent to {(pure f) <*> x} by the applicative laws, and since functions are curried, each use of <*> applies a single argument to the (potentially partially-applied) function.
> (sequence {(Just 1) :: (Just 2) :: (Just 3) :: Nil})
: (Maybe (List Integer))
(Just {1 :: 2 :: 3 :: Nil})
> (sequence {(Just 1) :: Nothing :: (Just 3) :: Nil})
: (Maybe (List Integer))
Nothing
> (traverse head {{1 :: Nil} :: {2 :: 3 :: Nil} :: Nil})
: (Maybe (List Integer))
(Just {1 :: 2 :: Nil})
> (traverse head {{1 :: Nil} :: Nil :: Nil})
: (Maybe (List Integer))
Nothing
2.3.7 Monads
typeclass
(class (Applicative m) => (Monad m)
[join : (forall [a] {(m (m a)) -> (m a)})] [=<< : (forall [a b] {{a -> (m b)} -> (m a) -> (m b)})])
Monads must satisfy the following laws:
{join . pure} = id {join . (map pure)} = id {join . join} = {join . (map join)}
The =<< operation is pronounced “bind”, and it is equivalent in power to join. While join is closer to the essence of what a monad is, =<< (or its flipped version, >>=) is more frequently used in ordinary programming. Either may be implemented, and a default implementation will be provided for the other, or an implementation may be provided for both if a more efficient implementation can be provided.
It is often more useful to use do than to use join or =<< directly.
Applies a function that produces a monadic value to a monadic value. The expression {f =<< x} is equivalent to (join {f <$> x}) (and an explicit implementation of both methods must maintain that law). It is worth comparing and contrasting the types of map/<$>, <*>, and =<<, all of which are similar but slightly different.Examples:
syntax
(do do-clause ... monadic-expr)
do-clause = monadic-do-clause | pure-do-clause monadic-do-clause = [binding-id <- monadic-expr] | monadic-expr pure-do-clause = (let binding-pair ...) | (letrec binding-pair ...)
The do form is desugared using the following rules:
Any use of do with a single subform expands to the subform—(do expr) is equivalent to expr.
Each monadic-do-clause introduces a use of >>=, with the result potentially bound to a binding-id. That is, (do [x <- m] more ...+) expands to {ma >>= (λ [x] (do more ...))}, and (do m more ...+) expands to {ma >>= (λ [_] (do more ...))}.
Each pure-do-clause produces a local binding form without any uses of >>=, which is useful to create local bindings that are not monadic. (do (let binding-pair ...) more ...+) expands to (let (binding-pair ...) (do more ...)), and (do (letrec binding-pair ...) more ...+) expands to (letrec (binding-pair ...) (do more ...)).
Using do is often much more readable than writing the uses of >>= out by hand, especially when it is helpful to give the result of each action a name.
> (do [xs <- (tail {1 :: 2 :: 3 :: 4 :: Nil})] [ys <- (tail xs)] [zs <- (tail ys)] (head zs))
: (Maybe Integer)
(Just 4)
> (do [xs <- (tail {1 :: 2 :: 3 :: Nil})] [ys <- (tail xs)] [zs <- (tail ys)] (head zs))
: (Maybe Integer)
Nothing
> (do (let [x 1] [y 2]) (println {"x is " ++ (show x)}) (println {"y is " ++ (show y)}) (let [z {x + y}]) (println {"x + y is " ++ (show z)}))
x is 1
y is 2
x + y is 3
: Unit
Unit