Coding guild Events Scala9 augustus 2017

August 1st, we had another Coding Guild session, about functional programming concepts. In one session, TypeClasses, Semigroups, Monoids, Functors and Applicatives were covered. The Cats library has been used, but not extensively been introduced. Quite heavy stuff, but with the excellent examples and exercises, prepared by Merlijn, Anton and Jeroen, it clarified a lot to me.

The material is so extensive that I will split it into a blog post per subject, and this one (as the title reveals) is about typeclasses.

On the internet there are a lot of resources about typeclasses. As typeclasses originate from the Haskell programming language, a nice introduction can be found in Learnyouahaskell:

A typeclass is a sort of interface that defines some behavior. If a type is a part of a typeclass, that means that it supports and implements the behavior the typeclass describes. A lot of people coming from OOP get confused by typeclasses because they think they are like classes in object oriented languages. Well, they’re not. You can think of them kind of as Java interfaces, only better.”

As Trivento, we focus on Scala as our (Functional) programming language, so let’s dive into some Scala code. All the code has been published on Github and is publicly available. We consider a simple domain of Cats and Cars, both making noise:

case class Car(make: String, model: String) {
  def soundAlarm: String = "weoweoweo!!!"
}
case class Cat(name: String) {
  def scream: String = "miauw!!!"
}

trait Data {
  val bmw = Car("BMW", "730i")
  val garfield = Cat("Garfield")
}

With some simple method overloading, it is possible to create a common method kick on both Cars and Cats, and based on the type of the object the correct noise will be triggered:

object SimpleMethodsOverloading extends NamePrintingApp with Data {
  def kick(cat: Cat): String = cat.scream.toUpperCase
  def kick(car: Car): String = car.soundAlarm.toUpperCase

  println(s"Kicking the BMW: ${kick(bmw)}")
  println(s"Kicking Garfield: ${kick(garfield)}")
}

which results on the console:

---- SimpleMethodsOverloading  ----
Kicking the BMW: WEOWEOWEO!!!
Kicking Garfield: MIAUW!!!

The code above does not scale. Moreover, it is not extensible by the outside world ***. Let us define a typeclass:

trait NoiseProducing[T] {
  def makeNoise(t: T): String
}

Typeclasses in Scala are defined as traits with a type parameter and functions for the type. Next we need to define implicit functions to implement the behaviour:

object Implicits {
  implicit val carNoise = new NoiseProducing[Car] {
    override def makeNoise(car: Car): String = car.soundAlarm
  }
  implicit val catNoise = new NoiseProducing[Cat] {
    override def makeNoise(cat: Cat): String = cat.scream
  }
}

Given the typeclass and the associated implicit functions, they can be used as follows:

object KickingMakesNoise extends NamePrintingApp with Data {
  import Implicits._

  // Give me a T and a function so that I can let the T make noise
  // This function is in NoiseProducing[T]
  def kickObject[T](obj: T)(implicit noiseProducing: NoiseProducing[T]): String =
    noiseProducing.makeNoise(obj).toUpperCase

  // Different syntax same result as above
  def kickObjectAltSyntax[T : NoiseProducing](obj: T): String =
    implicitly[NoiseProducing[T]].makeNoise(obj).toUpperCase

  println(s"Kicking the BMW: ${kickObject(bmw)(Implicits.carNoise)}")
  println(s"Kicking Garfield: ${kickObjectAltSyntax(garfield)}")
}

with the same result on the console when run:

---- KickingMakesNoise  ----
Kicking the BMW: WEOWEOWEO!!!
Kicking Garfield: MIAUW!!!

Well, admitted, not yet convincing that it’s worth all the effort … typeclasses are building blocks that will enable us to extend these kind of constructs. When dealing with Semigroups and Monoids it will become more apparent. A sneak preview of its potential is in the next example. Suppose, next to using NoiseProducing objects, we want to be able to wrap them into Options: so we will either have a None, Some(Car) or Some(Cat).

object MaybeKickingSomething extends NamePrintingApp with Data {
  import Implicits._
  import KickingMakesNoise.kickObject

  val noCar: Option[Car] = None
  val myCar: Option[Car] = Some(bmw)

  implicit def optionNoise[T: NoiseProducing] = new NoiseProducing[Option[T]] {
    override def makeNoise(t: Option[T]): String = t match {
      case Some(o) => kickObject(o)
      case None => "......"
    }
  }

  println(s"Kicking the non-existing car: ${kickObject(noCar)}")
  println(s"Kicking Some(BMW): ${kickObject(myCar)}")
  println(s"Kicking Some(Garfield): ${kickObject(Option(garfield))}")
  println(s"Kicking the plain BMW: ${kickObject(bmw)}")
}

with as result on the console when run:

---- MaybeKickingSomething  ----
---- KickingMakesNoise  ----
Kicking the non-existing car: ......
Kicking Some(BMW): WEOWEOWEO!!!
Kicking Some(Garfield): MIAUW!!!
Kicking the BMW again: WEOWEOWEO!!!

Note that we can still even use the plain, non-wrapped Car and Cat objects: the NoiseProducing typeclass ‘knows’ about Cars, Cats and about Options of both. This sheds already some light on its potential. Beware about what is still to come!

There is an exercise available:

package exercise0.exercise

object Domain {

  trait Encoder[A, B] {
    def encode(obj: A): B
  }

  trait Decoder[A, B] {
    def decode(obj: B): A
  }

  object Encoder {
    def encode[A, B](obj: A)(implicit encoder: Encoder[A, B]): B = encoder.encode(obj)
  }

  object Decoder {
    def decode[A, B](obj: B)(implicit decoder: Decoder[A, B]): A = decoder.decode(obj)
  }

  case class Person(name: String)
}

Develop an implementation for the String – Person encoding and decoding, based on the supplied typeclasses (Encoder and Decoder), such that the following tests do succeed:

package exercise0.answers

import org.scalatest.{FlatSpec, Matchers}

class Test extends FlatSpec with Matchers {
  import Domain._

  it should "encode and decode an object correctly" in {
    val person = Person("John")

    Decoder.decode(Encoder.encode(person)) should be(person)
  }
}

Answers are available in Github.