Sandbox project for the exercises in the book Scala with Cats. Based on the cats-seed.g8 template by Underscore.
Copyright Anonymous Aardvark. Licensed CC0 1.0.
Type classes are a Functional Programming (FP) construct (do not confuse with a regular class
) allowing the
description of an expected behavior over a type we don't own. In Scala, they are created through a type-parametrized
trait Example[A]
and by using implicit
s to define the expected behavior for each sub-type of A
.
Differently from OOP, type classes do not require any inheritance, nor even access to the code we want to set a
specific behavior. It requires, however, the language supports extension methods (implicit
s in Scala).
More details here.
- JsonExample: Basic example on scala
implicits
- PrintableLibrary: First exercise on type classes
- TypeclassesWithCats: How to use/declare typeclasses on Cats
A semigroup is a set (aka. type) equipped with a binary operation, such that:
- The operation must always have type
(A, A) -> A
- Integer addition is a suffices, because we can't add two integers and get a non-integer;
- Integer division doesn't, because there are at least two integers
a, b
such thata / b
is not an integer
- The binary operation must be associative
- Integer addition is associative because
a + (b + c) = (a + b) + c
- Integer subtraction is not associative because
a - (b - c) != (a - b) - c
- String concatenation is associative because
a ++ (b ++ c) == (a ++ b) ++ c
- Recall association means grouping, commutation means swapping:
- Association:
(a ++ b) ++ c = a ++ (b ++ c)
- Commutation:
(a ++ b) ++ c != (a ++ c) ++ b
- String concatenation, for instance, is associative, but not commutative.
- Association:
- Integer addition is associative because
A monoid is a semigroup containing an identity (empty) element on the set A
:
- Under integer addition,
0
is the empty (identity) element because for any integera
,a + 0 = a
- Under integer multiplication,
1
is empty element following the same rationale
- BooleanMonoids: Implements boolean operations as monoids
- MonoidAddition: Implements addition over list as a monoid
Are represented by T[_]
and are basically parametrized (1+) types, for instance, List[Int]
. In this, case we say
List
is a type constructor (because it takes a parameter), whereas List[Boolean]
is a type.
Functor is a type F[A]
with a method .map(A => B): F[B]
. It is as if we had a type A
wrapped by a
type F
, but when we do F[A].map(A => B)
, the result is still wrapped in F
again, hence F[B]
. Functors obey two
laws:
- Identity:
fa.map(a -> a) = fa
, aka. calling map with identity does nothing - Composition:
fa.map(g(f(_)) == fa.map(f).map(g) == (f o g)(fa)
Scala Function1[I, O]
can be seen as a functor if we fix the input parameter I
and allow the output
parameter O
to vary. Namely, let F*[O] = Function1[I, O]
, because I
is fixed. Then, we can see F*[O]
as a functor that can receive a function (to be used in the map) of type g: O => U
and return a F*[U]
. Under
the hood, what we have is F*[U] = F*[O].map(O => U) = F[I, O].map(O => U) = f(g(x))
.
Scala Function1
are functors: (func1 map func2)(x)
, thus allowing lazy operation chaining. Besides, Cats allows
creating functors for any single-parameter kind.
Formalities aside, the underlying idea of functors consists in chaining operations. The F*[O]
idea from previous
section revolves around the fact it represents a function f: I => O
with a fixed input type I
. Since it's a functor,
we can chain (map) it with another function g: O => U
using F*[O].map(g: O => U)
and get back an F*[U]
which, in
the end of the day corresponds to a function chaining (f o g)(x) = f(g(x)): I => U = F*[U]
.
If instead of appending a function we want to prepend one, F*[O]
must provide a .contramap(? => I)
method and, if it
does, then it will be called Contravariant Functor. Under this arrangement, we can have another function e: H => I
and get a F'[O] = F*[O].contramap(H => I)
. Notice .contramap
yields a new type F'[O] = F[J, O]
instead
F*[O] = F[I, O]
. In a nutshell, contravariant functors allow, through contra-map, function composition by the left,
instead of the usual right: (e o f)(x) = e(f(x)): J => O = F'[O]
.
Explanation is hard because Cats' functor type is a 1-Kind type, for less-formal and 2-kind type this Stack Overflow top answer may be way easier to understand.
Invariant functors provide a method F[A].imap(a: A => B, b: B => A): F[B]
where a
and b
are conversion functions
between domains. By specifying a
and b
it's possible to construct a new functor type F[B]
.
- Covariant type: means
B
is a subtype ofA
, hence we can always up-cast/coerce/replaceB
intoA
. By analogy, ifF
is a (covariant) functor, whenever we have aF[B]
and a conversionB => A
, it's possible to obtain anF[A]
. - Contravariant type: is the opposite, meaning
B
is a supertype ofB
. By analogy, a contravariant functorF[A]
allows providing a mappingZ => A
and yields a new functorF[Z]
. - Invariant functor: combines the case where we can map from
F[A] => F[B]
via a functionA => B
and vice-versa.
- FunctorExamples: Basic functor examples with function chaining
- BranchingWithFunctor: Implements function chaining as functors
- PrintableContramap: Shows how to define a new
Functor[B]
in terms of an already-existingFunctor[A]
though contra-maps for the already-seenPrintable[A]
type. - InvariantFunctorCodec: Shows how bidirectional functors and imap works.
- CatsFunctors: Shows how to use Cats' functors.
TODO