# Type Classes in Scala 3

I'm recently migrating some libs and projects to Scala 3, I guess it would be very helpful to me or anyone interested to learn some new functional programming features that Scala 3 is bringing to us.

Source code 👉 https://github.com/jcouyang/meow

Instead of just introducing the concepts, let's try something more practical this time: redesigning a Category Theory library for Scala 3.

I like to call it *Meow* because it makes lib user forget about **Cat** (egory) itself and focus on its traits.

First, let's recap some Scala 3 new syntax.

## Implicits

### given

Defining a type class in Scala 3 is less boilerplate than in Scala 2.

For instance, a Functor:

trait Functor[F[_]]: def fmap[A, B](f: A => B): F[A] => F[B]

This is basically the same as in Scala 2, but the cool part comes when you implement the type class.

An `Option`

is mappable, so there exist a `Functor`

instance for `Option`

:

object Functor: given Functor[Option] with def fmap[A, B](f: A => B): Option[A] => Option[B] = (oa: Option[A]) => oa.map(f)

Let me just remind you how it was done in Scala 2:

object Functor { implicit val functorOption: Functor[Option] = new Functor[Option] { def fmap[A, B](f: A => B): Option[A] => Option[B] = (oa: Option[A]) => oa.map(f) } }

- No longer require a name, implicits are mostly for compiler, type should just explanatory enough
- No longer require
`new`

### using

Using a type class instance is now via `using`

instead of `implicit`

, which is less confusing because implicit could mean
different in different places.

Let's say we like a global universal function `map`

, that can map any data type which has a Functor instance:

1: def map[F[_]] = 2: [A, B] => 3: (f: A => B) => 4: (using functor: Functor[F]) => 5: (fa: F[A]) => functor.fmap(f)(fa)

- line 2 was introduced in Rank N Types in Scala 3
- line 4 is the new syntax, just replace
`implicit`

with`using`

.

In this function, we also delay the `[A, B]`

type parameter and context of `using functor: Fucntor[F]`

, so that you can:

- pass
`map[Option]`

around, as`[A, B]`

is not yet set. - pass
`map[Option](f)`

around, without immediately providing a Functor instance.

There are some other forms of `using`

, i.e. you can omit the `using`

keyword if it is in a lambda, by replacing `=>`

with `?=>`

https://dotty.epfl.ch/docs/reference/contextual/context-functions.html
.

def map[F[_]] = [A, B] => (f: A => B) => (functor: Functor[F]) ?=> (fa: F[A]) => functor.fmap(f)(fa)

Or, omit the name of the instance:

def map[F[_]] = [A, B] => (f: A => B) => (using Functor[F]) => (fa: F[A]) => summon[Functor[F]].fmap(f)(fa)

`summon`

is the new `implicitly`

.

That is pretty much what we need to know to start building Meow.

## Hierarchy

Meow has a significantly different design than Cats, namely the type class hierarchy, which Meow doesn't have at all.

Meow uses context bounding to define type class dependencies, similar to Haskell.

Such as, `Applicative`

, which from Cats implementation, looks like:

trait Applicative[F[_]] extends Functor[F] { def pure[A](a: A): F[A] def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] }

The trick here is OO style `extends`

, when `Applicative[F] extends Functor[F]`

, `Applicative`

is also mappable.

This save lib maintainer some boilerplate, e.g. `Monad`

probably `extends`

most of typeclasses
If you follow the arrow on the graph it extends `FlatMap`

, `Applicative`

, `Apply`

, `Functor`

, `Semigroupal`

, `Invariant`

.
, so they just
implement `Monad`

for `Option`

for instance, and no need to implement `Applicative[Option]`

, `Functor[Option]`

…

But this kind of OO solution has many uncertainties, override introduces mutability.

When I call `*>`

on `Option`

, which implementation am I actually using?

The `Monad[Option]`

? or `Applicative[Option]`

? Both instances could have implemented `ap`

, and as a lib user, I have to
build the hierarchy graph in my mind, to determinate, oh, `Monad`

extends `Applicative`

, and `Option`

has `Monad`

instance, so
it must use the `Monad`

's `ap`

.

Also, even for lib maintainer it is too coupled with the graph, anything changed at the top-level typelass, will cause a chain reaction to whatever extends it.

Instead of using OO style to add capability via `extends`

, we can simply declare type class `Applicaitve`

in a context where `Functor[F]`

exists.

trait Applicative[F[_]:Functor]: def pure[A](a: A): F[A] def liftA2[A, B, C](f: A => B => C): F[A] => F[B] => F[C]

By using context bound `F`

must have a `Funtor`

instance, we are definitely sure(so is the compiler) when I `fmap`

something,
it must use a `Functor`

instance, because there are 0 overlaps from `Applicative`

instance.

More importantly, users are less confused and less thing to memorize, of which method belongs to which type class Or, should they even care? .

Now we can even safely define functions in global.

def map[F[_]] = [A, B] => (f: A => B) => (functor: Functor[F]) ?=> (fa: F[A]) => functor.fmap(f)(fa) def pure[F[_]] = [A] => (a: A) => (applicative: Applicative[F]) ?=> applicative.pure(a) def liftA2[F[_]] = [A, B, C] => (f: A => B => C) => (A: Applicative[F]) ?=> A.liftA2(f) def flatMap[M[_]] = [A, B] => (f: A => M[B]) => (monad: Monad[M]) ?=> (ma: M[A]) => monad.bind(f)(ma)

This enabled a more user friendly interface, as they no longer need to know anything about the type class definitions and which method belongs to which type class, they only need to memorize few useful functions, that's it.

- Option is map-able:

map[Option]((a:Int) => a + 1)(Option(1))

- Option is pure-able and apply-able:

val fa = pure[Option](1) val fb = pure[Option](2) val f = (x: Int) => (y: Int) => x + y assertEquals(liftA2(f)(fa)(fb), Option(3))

- Option is flatMap-able

val fa = pure[Option](1) val ff = (x:Int) => Option(x +1) assertEquals(flatMap[Option](ff)(fa), Option(2))

Users don't even need to aware of type classes exist, for them these are just a few handy functions help dealing with data types.

## Extensions

Once again, I want to emphasize that type classes are not for users, type classes are just a technique for lib authors to abstract and organize traits, the purpose is not just define `map`

, `flatMap`

, they are the building blocks for us to extend the capability of data type.

For example, if we implement `Functor[Option]`

, `map`

is the only function we need to implement, but from there we will also get a bunch of functions for free, thanks to `extension`

:

trait Functor[F[_]]: def fmap[A, B](f: A => B): F[A] => F[B] extension [A, B](fa: F[A]) infix def map(f: A => B): F[B] = fmap(f)(fa) @targetName("mapFlipped") def <#>(f: A => B): F[B] = fmap(f)(fa) @targetName("voidLeft") infix def `$>`(a: B): F[B] = fmap(const[B, A](a))(fa) def void: F[Unit] = fmap(const[Unit, A](()))(fa) end Functor

As you can see `map`

, `mapFlipped`

, `voidLeft`

, `void`

are all dependent and only dependent on `fmap`

, by implementing `fmap`

, you get all of
these function for **free**.

`@targetName`

https://dotty.epfl.ch/docs/reference/other-new-features/targetName.html is a new Scala 3 annotation, it allow us to define an alternate name for the implementation of that definition. It is recommended that definitions with symbolic names have a`@targetName`

.

And you can add even more extensions, to its companion object too.

object Functor: extension [F[_], A, B](f: A => B) @targetName("fmap") def `<$>`(fa: F[A])(using Functor[F]): F[B] = fa map f extension [F[_], A, B](a: A) @targetName("voidRight") def `<$`(fb: F[B])(using Functor[F]): F[A] = fb.map(const(a))

Quiz:Guess what it will print? 2? 3? or 4?Option(1) `$>` 3 `<$` Option(2) <#> (_ + 1)

With extensions, the `Option`

examples above can be rewritten to be more Haskell-ish:

- Option is
`<$>`

-able:

(a:Int) => a + 1 `<$>` Option(1)

- Option is pure-able and
`<*>`

-able:

val fa = pure[Option](1) val fb = pure[Option](2) val f = (x: Int) => (y: Int) => x + y assertEquals(f `<$>` fa <*> fb, Option(3))

- Option is
`>>=`

-able

val fa = pure[Option](1) val ff = (x:Int) => Option(x +1) assertEquals(fa >>= ff, Option(2))

## Prelude

Since all functions `map`

, `liftA2`

, `flatMap`

… can be just global static, it is
also safe to export all these functions to a single place – `prelude`

, with Scala 3's
new feature `export`

https://dotty.epfl.ch/docs/reference/other-new-features/export.html
, which is also much cleaner than Cats `extends`

approach.

object prelude: export data.Functor.{given,_} export control.Applicative.{given,_} export control.Monad.{given,_}

Then user can just `import meow.prelude.{given,*}`

.

With all the new Scala 3 features, implementing type classes has never been so clean.

There are more type classes implementations and usage examples in https://github.com/jcouyang/meow.
You can try them out by cloning the repo and `sbt test`

.

To be continued… in the next blogpost, I'll explain how to implement generic type class deriving without Shapeless in Scala 3.

## Footnotes:

^{2}

If you follow the arrow on the graph it extends `FlatMap`

, `Applicative`

, `Apply`

, `Functor`

, `Semigroupal`

, `Invariant`

.

^{3}

Or, should they even care?