1. Introduction

Value lambdas, or lambda functions, define functions from values to values. Type lambdas define functions from types to types. They are similar to value lambdas, but they work at a higher level.

In this tutorial, we’ll learn about type lambdas and understand what they are and when we might find them useful.

2. Types and Kinds

Kinds are quite a difficult topic in computer science and type theory, and a thorough explanation is out of the scope of this article. However, a quick look at what kinds are is useful to understand type lambdas.

Non-generic types, such as String, Int, Boolean, and so on, are the simplest types of kind. This is because we can instantiate them without providing any type parameters. Hence, we can see them as “concrete” types.

Then, we have generic types, such as List[T] and Option[T]. For us to instantiate a generic type, we have to specify a type argument. In other words, we have to explicitly say the type of the elements of a List or an Option. Hence, generic types are also known as higher-level types. They form another kind.

Lastly, higher-kinded types are generic types whose type arguments are also generics. For example, Cats defines its Functor typeclass as class Functor[F[_]]. In this case, the type parameter expected by Functor is itself a generic type (F[_]). Theoretically, we can add as many levels as we want to a higher-kinded type.

3. What Are Type Lambdas?

In this section, we’ll see how to work with type lambdas in Scala 3. We’ll first see what the syntax for type lambdas looks like, and then we’ll move to a more advanced concept –  currying with type lambdas.

3.1. Working With Type Lambdas

As we saw above, higher-level and higher-kinded types can’t be instantiated on their own, as we have to provide them with the desired type parameter(s) of inferior kinds first. For instance, to instantiate an Option[T], we have to specify what T is: an Int, a String, and so on.

For this reason, we can think of the generic Option type as a type constructor, i.e., a function inputting a lower-kinded type and returning a lower-kinded type. This is what we call “type lambda”.

Let’s see how we can express it in Scala 3:

type MyTry = [X] =>> Either[Throwable, X]

Not surprisingly, this looks very much like a function. The definition above essentially inputs a type argument X and evaluates to the type Either[Throwable, X]. Let’s see how we can use it in practice:

type MyTry = [X] =>> Either[Throwable, X]

@main def main(): Unit = {
  val myTryInt: MyTry[Int] = Right(10)
  val myTryString: MyTry[String] = Right("Baeldung")
  val myTryLeft: MyTry[Int] = Left(Exception("Boom"))

  println(myTryInt)
  println(myTryString)
  println(myTryLeft)
}

In the example above, we instantiated MyTry three times. We used the following syntax to specify the type of X: MyTry[Int] and MyTry[String]. In particular, myTryInt and myTryLeft are of type Either[Throwable, Int], whereas myTryString is of type Either[Throwable, String]. Hence, we can instantiate them using the Left and Right constructors of Either.

Running the example above will print:

Right(10)
Right(Baeldung)
Left(java.lang.Exception: Boom)

The type parameters of a type lambda may have bounds, but they may not carry variance annotations.

3.2. Currying With Type Lambdas

Similarly to value lambdas, type lambdas may be curried:

type MyTuple = [X] =>> [Y] =>> (X, Y)

As with all the other functions, type lambdas also come with type-checking and subtyping rules. Nonetheless, those topics are outside the scope of this article.

4. Why Do We Need Type Lambdas?

Essentially, type lambdas let us define higher-kinded types leaving some types as “blank”. Let’s consider, for example, the MyTry type we defined above. In that case, we set the “left” projection of Either to Throwable. However, we didn’t specify the “right” projection of Either. Instead, we defined a type lambda to let users choose it.

This gives us a great level of flexibility. As type lambdas can be curried and carry type bounds, we can use them to express a wide range of higher-kinded types with a pretty readable and maintainable syntax.

Before Scala 3, API designers had to resort to compiler plugins, namely kind-projector, to achieve the same level of expressiveness. That plugin is still maintained for Scala 2, but Scala 3 features support for type lambdas out of the box. Furthermore, its syntax is compatible with that of kind-projector.

Besides API design, type lambdas are a more niche feature of the Scala language that we’ll likely seldom use.

5. Conclusion

In this article, we took a first look at type lambdas. First, we briefly discussed how Scala provides different sorts of kinds including concrete types, generics, and also higher-kinded types. Then, we experimented with type lambdas, seeing what they are and when we might find them useful.

As usual, the source code for the examples is available over on GitHub.