Daniel Ciocîrlan
4 min read •
Share on:
This article is a bit more difficult — it’s aimed at the experienced Scala developer who can think at a higher level. Ideally, you’ve read the previous article - it serves as a prep for this one. Type lambdas are simple to express in Scala 3, but the ramifications are deep.
This feature (along with dozens of other changes) is explained in depth in the Scala 3 New Features course.
We discussed in the previous article about categorizing types in Scala into kinds. Scala 3 is no different here. However, it introduces a new concept and a syntactic structure to express it, which might look daunting and hard to wrap your head around.
To quickly recap:
Int
, String
or your own non-generic classes belong to the value-level kind — the ones you can attach to values.List
belong to what I called the level-1 kind — they take plain (level-0) types as type arguments.As I mentioned before, generic types need the appropriate type arguments before they can be attached to a value. We can never use the List
type directly to a value, but only List[Int]
(or some other concrete type).
You can therefore think of List
(the generic type itself) as similar to a function, which takes a level-0 type and returns a level-0 type. This “function” from level-0 types to level-0 types represents the kind which List
belongs to. In Scala 2, representing this such a type was horrible ({ type T[A] = List[A] })#T
, yuck). In Scala 3, it looks much more similar to a function:
[X] =>> List[X]
Read this structure as “a type that takes a type argument X
and results in the type List[X]
”. This does the exact same thing as the List
type (by itself): takes a type argument and results in a new type.
Some more examples in increasing order of complexity:
[T] =>> Map[String, T]
is a type which takes a single type argument T
and “returns” a Map
type with String
as key and T
as values[T, E] =>> Either[Option[T], E]
is a type which takes two type arguments and gives you back a concrete Either
type with Option[T]
and E
[F[_]] =>> F[Int]
is a type which takes a type argument which is itself generic (like List
) and gives you back that type, typed with Int
(too many types, I know)Type lambdas become important as we start to work with higher-kinded types. Consider Monad, one of the most popular higher-kinded type classes. In its simplest form, it looks like this:
trait Monad[M[_]] {
def pure[A](a: A): M[A]
def flatMap[A, B](m: M[A])(f: A => M[B]): M[B]
}
You might also know that Either
is a monadic data structure (another article on that, perhaps), so we can write a Monad
for it. However, Either
takes two type arguments, whereas Monad
requires that its type argument take only one. How do we write it? We would like to write something along the lines of
class EitherMonad[T] extends Monad[Either[T, ?]] {
// ... implementation
}
In this way, this EitherMonad
could work for both Either[Exception, Int]
and Either[String, Int]
, for example (where Int
is the desired type). Given an error type E
, we’d like EitherMonad
to work with Either[E, Int]
whatever concrete E
we might end up using.
Sadly, the above structure is not valid Scala.
The answer is that we would write something like
class EitherMonad[T] extends Monad[[E] =>> Either[T, E]] {
// ... implementation
}
It’s as if we had a two-argument function, and we needed to pass a partial application of it to another function.
If this is really abstract and hard to wrap your head around, I feel ya.
Prior to Scala 3, libraries like Cats used to resort to compiler plugins (kind-projector) to achieve something akin to the ?
structure above. Now in Scala 3, it’s expressly permitted in the language.
With a simple syntactic structure, Scala 3 solved a problem that API designers had been facing (and bending over backwards) for ages - how to define higher-kinded types where some type arguments are left “blank”.
In a future article, we’ll talk about some more advanced capabilities (and pitfalls) of type lambdas.
Share on: