One of the simplest and most recognizable type classes is the semigroup. This type class abstracts over the ability to combine values of a certain type in an associative manner.
cats.Semigroup[A] to model semigroups.
combine method takes two values of the type
A and returns an
In addition, Cats defines syntax allowing the binary operator
|+| to be
used in place of the
Here is a small method that provides a generic way to combine the elements of a list:
import cats.Semigroup import cats.implicits._ def gsum[A: Semigroup](values: List[A]): Option[A] = if (values.isEmpty) None else Some(values.reduceLeft((x, y) => x |+| y))
(A similar method is built into Cats as
One of the parts of
gsum that might be hard to understand is where
|+| method comes from. Since
y are totally generic values
A) how can we call a method on them?
To boil the example down further, consider this simpler example:
import cats.implicits._ 19 |+| 20 // produces 39
How does this work? We know that the
Int type does not have a
Experienced Scala developers will suspect that implicits play a role here,
but what are the details?
Let’s walk through how the expression
19 |+| 20 is compiled.
|+| method is needed on
does not provide one, the compiler searches for an implicit conversion to a
type that does have a
Due to our import, it will find the
method, which returns a type that has a
|+| method (specifically
semigroupSyntax requires an implicit
Semigroup[A] value to be in scope.
Do we have a
Semigroup[Int] in scope?
Yes we do. Our import also provides an implicit value
AdditiveCommutativeGroup[Int]. Leaving aside what additive, commutative,
and group mean here, this is a subtype of
Semigroup[Int], so it matches.
Let’s continue with our current example. At this point we have gone from:
19 |+| 20 // produces 39
semigroupSyntax[Int](19)(intGroup) |+| 20
But we aren’t out of the woods yet! We still need to see how this expression is evaluated.
Looking at how the
|+| method is
reveals the cryptic
macro Ops.binop[A, A]. What is this?
Following the rabbit hole farther, we come to
which provides the macro implementation that
|+| is using. Aside from a
item in the
operatorNames map, we don’t have any clues what is going on.
The machinist project was created
to optimize exactly this kind of implicit syntax problem. What will happen here
operatorNames describes how to rewrite expressions using type
classes. Long-story short, we will transform:
semigroupSyntax[Int](19)(intGroup) |+| 20
The aforementioned suggestive map item tells us that we should rewrite the
to method calls on the given type class (i.e.
Just to confirm that we’re done, let’s look at what
intGroup.combine will do.
We started with a call to
AdditiveCommutativeGroup[Int], which will find
Then we call the
method on it to produce a
So putting that together, we can see that calling
intAlgebra.plus(19, 20), and that this is defined as
19 + 20,
as we would expect.
This is a lot of machinery. The incredibly terse and expressive syntax it enables is quite nice, but you can see that even leaving out one import will cause the whole edifice to come tumbling down.
The easiest way to use Cats is to just import
way, you can be sure that you have all of it. There are individual imports
cats.std which can be used to pinpoint the exact
values and method you want to put into scope, but getting these right
can be a bit tricky, especially for newcomers.
Some more examples of Machinist can be found in the README.
You may also decide that the syntax convenience is not worth it. To write our original example without syntax implicits (but still using implicit values) you could say:
import cats.Semigroup import cats.implicits._ def gsum[A](values: List[A])(implicit s: Semigroup[A]): Option[A] = if (values.isEmpty) None else Some(values.reduceLeft((x, y) => s.combine(x, y))) // values.reduceLeft(s.combine) would also work
Whether to use syntax implicits or explicit method calls is mostly a matter of preference. Personally, I like using syntax explicits to help make generic code read in a clearer manner, but as always, your mileage may vary.
Unless otherwise noted, all content is licensed under a Creative Commons Attribution 3.0 Unported License.Back to blog