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 implicits 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 (implicits 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, bsuch thata / bis 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,
0is the empty (identity) element because for any integera,a + 0 = a - Under integer multiplication,
1is 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
Bis a subtype ofA, hence we can always up-cast/coerce/replaceBintoA. By analogy, ifFis 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
Bis a supertype ofB. By analogy, a contravariant functorF[A]allows providing a mappingZ => Aand yields a new functorF[Z]. - Invariant functor: combines the case where we can map from
F[A] => F[B]via a functionA => Band 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