Typeclasses in scala 3
Scala 3 introduces a number of quality of life improvements, among which is a refined syntax for typeclasses. Today, we will take a quick look at much improved syntax for typeclasses.
Type classes can be thought of as a way of abstracting over different types that share a common behaviour. let's focus on a simple example: monoid. A monoid is an object which satisfies: associativity & identity. We will look at an instance of monoid for a String. A string satisfies the monoid laws by being associative: "a" ++ ("b" ++ "c") is the same as ("a" ++ "b") ++ "c". and also identity via the empty string "".
First start by defining the trait
trait Monoid[M]:
extension (m: M) def mempty: M
extension (m: M) def combine(m2: M): M
We define two methods: mempty and combine. "mempty" represents identity. "combine" acts as our binary operation which combines two types M and produces a combined type M
Creating an instance for type String
given Monoid[String] with
extension (s: String) def mempty: String = ""
extension (s: String) def combine(str2: String): String = s ++ str2
Here, we define an instance of monoid for the type String. It satisfies the monoid laws as mempty is simply an empty string "" and combine for type String is defined as concatenation.
Previously in scala 2, one would implement the above as:
trait Monoid[M] {
def combine(m: M, m2: M): M
def mempty: M
}
object Monoid {
implicit val stringMonoid: Monoid[String] = new Monoid[String] {
def combine(m: String, m2: String): String = m ++ m2
def mempty: String = ""
}
}
Comparing this to the Scala 2 approach, the definition becomes more concise and cleaner in Scala 3. This is thanks to the use of extension methods and the "given" syntax to declare instances of typeclasses compared to Scala 2's implicits