First, we define the type classes, which are just trait
s in scala:
// Type classes for Semigroup and Monoid (we will be using monoid, but this is more formal)
trait Semigroup[A] {
def op(x: A, y: A): A // should be associative
}
trait Monoid[A] extends Semigroup[A] {
def zero: A
}
Next, we define the instances of the type class(es) just by enclosing them in an object to provide implicit vals:
// Type class instances
object MonoidInstances { // better a trait to be able to mix in
implicit val sumInt: Monoid[Int] = new Monoid[Int] {
override def zero = 0
override def op(x: Int, y: Int) = x+y
}
}
Mind these implicits have to be in the scope when using it. This is ok to implement as an object
when using them in the REPL, but better thing could be to implement them as a trait
to be mixable in.
With the implicit values we can define already operations:
// Ops (in this case only for Int)
object Operators { // again better a trait to be able to mix in
def sum[A](l: List[A])(implicit monoid: Monoid[A]): A = l match {
case l if l.isEmpty => monoid.zero
case l@xs :: rest => monoid.op(xs, sum(rest))
}
}
Here we are defining one operation for a List
type (actually, a List
type constructor). As we have implemented only an
implicit value for Monoid[Int]
, we will be able to do only Operators.sum(l)
if l.isInstanceOf[List[Int]] == true
.
Also, mind we are doing (implicit monoid: Monoid[A])
, which means the implicit values has to be in the scope when sum
is called, as the actual method signature involves implicitely a Monoid[A]
instance. This can be achieved by import MonoidInstances._
or, if a trait
, by mixing it in. Otherwise, we should have to do sum(l)(MonoidInstances.sumInt)
to pass in the instance explicitely.
The last thing we can do, is an extension method to provide new syntax for the List
type by using implicit classes. This extends the syntax for a type, just like:
// Syntax (extension methods)
object SumupSyntax {
implicit class Sumup[A](val l: List[A]) extends AnyVal {
def sumup(implicit monoid: Monoid[A]): A = Operators.sum(l)
}
}
Then, we could do just l.sumup
to get the same result as if doing Operators.sum(l)
. Again, for this implementation, this only works for List[Int]
and again the Monoid[Int]
instance has to be in the scope (otherwise, it won't even compile). Also we use a value class to avoid runtime allocation (https://docs.scala-lang.org/overviews/core/value-classes.html).
Actually, the minimal implementation for an extension method is:
class OddChecker(val value: Int) {
def isOdd: Boolean = value % 2 != 0
}
object OddOperations { // we need an object wrapper if working in the REPL, otherwise a trait would be better
implicit def actuallyNoMatterTheName(i: Int) = new OddChecker(i)
}
import OddOperations._
assert(19.isOdd == true)
assert(20.isOdd == false)
The compiler checks the implicit
for the type Int
when calling the isOdd
.
A short and quick extension method for any type (proved target types and methods match) is like that (proved we have the implicit in the scope):
and then the following can be done:
As a bonus we are adding extended syntax here, actually for any type (as type
T
is unbound, also in theOption
class definition, which is covariant:Option[+A]
).