Scala - Type Lambdas


Type lambdas provide higher-kinded types directly without defining a new type. You can partially apply type constructors for more flexibility and abstraction. Type lambda lets you express a higher-kinded type directly. For example, the following type lambda defines a binary type constructor that maps arguments X and Y to Map[Y, X] -

[X, Y] =>> Map[Y, X]

Type lambdas can have bounds on type parameters but cannot carry variance annotations (+ and -)

Syntax

The syntax for type lambdas is -

Type            ::=  ... |  TypeParamClause '=>>' Type
TypeParamClause ::=  '[' TypeParam {',' TypeParam} ']'
TypeParam       ::=  {Annotation} (id [HkTypeParamClause] | '_') TypeBounds
TypeBounds      ::=  ['>:' Type] ['<:' Type]

Example of Type Lambdas

This is type lambda that represents a binary type constructor. This maps arguments X and Y to Map[Y, X]

type MyTypeLambda = [X, Y] =>> Map[Y, X]

Type Checking and Subtyping

Type lambda, like [X] =>> F[X] defines a function from types to types. You may have bounded parameters. For example, [X >: L <: U] =>> F[X] checks that arguments conform to the bounds L and U.

For subtyping, consider two type lambdas -

type TL1  =  [X >: L1 <: U1] =>> R1
type TL2  =  [X >: L2 <: U2] =>> R2

Then TL1 <: TL2 if -

  • The type interval .U2 is contained in L1..U1 (i.e., L1 <: L2 and U2 <: U1).
  • R1 <: R2.

Relationship with Parameterized Type Definitions

A parameterized type definition -

type T[X] = R

is equivalent to:

type T = [X] =>> R

If the type definition carries variance annotations. These must be satisfied by the type lambda.

Example

An example of using type lambdas to partially apply type constructor -

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

type MapWithIntKey[A] = Map[Int, A]

implicit val mapFunctor: Functor[MapWithIntKey] = new Functor[MapWithIntKey] {
  def fmap[A, B](fa: Map[Int, A])(f: A => B): Map[Int, B] = fa.mapValues(f).toMap
}

object Demo {
  def main(args: Array[String]): Unit = {
    val map: Map[Int, String] = Map(1 -> "one", 2 -> "two")
    val mappedMap = mapFunctor.fmap(map)(_.toUpperCase)
    println(mappedMap)
  }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines a Functor instance for MapWithIntKey. So you can map functions over Map[Int, A] value.

This will produce the following result -

Map(1 -> ONE, 2 -> TWO)

Type lambdas are used to solve the problem of partially applying types, like in higher-kinded type contexts.

Functor for a Partially Applied Type

There is Functor for type that takes two parameters, like Map. Generally, you cannot directly use Map as Functor because it takes two type parameters. Using type lambdas, you can partially apply Map to create a single-parameter type constructor -

// Functor type class
trait Functor[F[_]] {
  def fmap[A, B](fa: F[A])(f: A => B): F[B]
}

// Provide instance of Functor for Map with fixed key type
implicit def mapFunctor[A]: Functor[({ type L[B] = Map[A, B] })#L] = new Functor[({ type L[B] = Map[A, B] })#L] {
  def fmap[B, C](fa: Map[A, B])(f: B => C): Map[A, C] = fa.map { case (k, v) => (k, f(v)) }
}

// Implicit class to add fmap syntax
implicit class FunctorOps[F[_], A](fa: F[A])(implicit F: Functor[F]) {
  def fmap[B](f: A => B): F[B] = F.fmap(fa)(f)
}

// Demo object
object Demo {
  def main(args: Array[String]): Unit = {
    val map: Map[Int, String] = Map(1 -> "one", 2 -> "two")
    val mappedMap = map.fmap(_.toUpperCase)
    println(mappedMap)
  }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines a Functor instance for partially applied Map. So you can map a function over a Map[A, B] value.

This will produce the following result -

Map(1 -> ONE, 2 -> TWO)

Higher-Kinded Type Class with Type Lambdas

If you want to define a type class Bifunctor that works with types. These take two type parameters -

trait Bifunctor[F[_, _]] {
  def bimap[A, B, C, D](fab: F[A, B])(f: A => C, g: B => D): F[C, D]
}

object Bifunctor {
  implicit val mapBifunctor: Bifunctor[Map] = new Bifunctor[Map] {
    def bimap[A, B, C, D](fab: Map[A, B])(f: A => C, g: B => D): Map[C, D] = {
      fab.map { case (k, v) => (f(k), g(v)) }
    }
  }
}

object Demo {
  def main(args: Array[String]): Unit = {
    val map: Map[Int, String] = Map(1 -> "one", 2 -> "two")
    val bimapResult = Bifunctor.mapBifunctor.bimap(map)(_.toString, _.toUpperCase)
    println(bimapResult)
  }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code defines a Bifunctor instance for Map. So you can map two functions over the keys and values of a Map.

This will produce the following result -

Map(1 -> ONE, 2 -> TWO)

Curried Type Parameters

Note that Scala currently does not support curried type parameters directly. But you can achieve similar functionality using type lambdas.

Example

Here is an example of curried type parameters in Scala:

import scala.language.higherKinds
import scala.language.implicitConversions

// Functor type class
trait Functor[F[_]] {
  def fmap[A, B](fa: F[A])(f: A => B): F[B]
}

// CurriedType type alias using the correct Scala 3 syntax
type CurriedType[A] = [B] =>> Map[A, B]

// Provide instance of Functor for CurriedType
implicit def curriedTypeFunctor[A]: Functor[CurriedType[A]] = new Functor[CurriedType[A]] {
  def fmap[B, C](fa: Map[A, B])(f: B => C): Map[A, C] = fa.view.mapValues(f).toMap
}

object Demo {
  def main(args: Array[String]): Unit = {
    val map: Map[Int, String] = Map(1 -> "one", 2 -> "two")
    val mappedMap = curriedTypeFunctor[Int].fmap(map)(_.toUpperCase)
    println(mappedMap)
  }
}

Save the above program in Demo.scala. Use the following commands to compile and execute this program.

Command

> scalac Demo.scala
> scala Demo

Output

The above code has functor instance for curried type Map[A, B].

This will produce the following result -

Map(1 -> ONE, 2 -> TWO)

Type Lambdas Summary

  • Type lambdas can express higher-kinded types directly without defining a new type.
  • Type lambdas can be used to partially apply type constructors.
  • These are used in contexts where higher-kinded types are needed, like in defining typeclasses like Functor.
  • Kind Projector provides clear syntax for type lambdas for easier to use.
  • You can simulate curried type parameters using type lambdas for more complex type abstractions.