Skip to content

Instantly share code, notes, and snippets.

@telekosmos
Last active May 1, 2022 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save telekosmos/6baf3de2e1f1abf90d840085f64d7f5c to your computer and use it in GitHub Desktop.
Save telekosmos/6baf3de2e1f1abf90d840085f64d7f5c to your computer and use it in GitHub Desktop.
Type classes + implicit values + extension methods

First, we define the type classes, which are just traits 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.

@telekosmos
Copy link
Author

telekosmos commented Apr 26, 2022

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):

implicit class ToSome[T](t: T) {
  def toSome: Option[T] = Some(t)
}

and then the following can be done:

scala> "string".toSome
val res0: Option[String] = Some(string)

scala> -123.toSome
val res1: Option[Int] = Some(-123)

scala> 2.53.toSome
val res2: Option[Double] = Some(2.53)

As a bonus we are adding extended syntax here, actually for any type (as type T is unbound, also in the Option class definition, which is covariant: Option[+A]).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment