Type Lambdas & Higher Kinds

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Higher-kinded type parameters (F[_]) exist since Scala 2.8, and type lambdas and AnyKind have been part of Scala 3 since 3.0, so everything here also works on the 3.9 LTS (3.9.0, 2026-09-03) and the 3.3 LTS, except for the -Xkind-projector option, which is described in its own section. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails.

A type constructor is a type that needs type arguments to become a type you can have values of: List is not a type of any value, List[Int] is. Scala lets a type parameter stand for a type constructor, which is what the functional libraries use to write one map that works for lists, options and maps. This page covers the notation for those parameters, the type lambda that builds a constructor from another one, and the escape hatch that accepts a type of any shape. For plain type parameters see Generics & Bounds; for classes of operations over such constructors see Type Classes.

Kinds

A kind classifies a type the way a type classifies a value. Writing * for an ordinary type:

Type Kind Reading

Int, List[Int]

*

a proper type; values exist

List, Option

* -> *

takes one type, gives a proper type

Either, Map

* -> * -> *

takes two types

Functor (below)

(* -> *) -> *

takes a type constructor

The compiler checks that the argument supplied for a type parameter has the kind that the parameter declares. A declaration F[_] says “F` is of kind * -> *”; `F[_, _] says * -> * -> *.

Higher-kinded type parameters

A trait or method with a parameter written F[_] abstracts over a type constructor. The underscore is a placeholder; the real name of the inner parameter is not used.

//> using scala 3.10.0

trait Container[F[_]]:
  def empty[A]: F[A]
  def add[A](fa: F[A], a: A): F[A]

object ListContainer extends Container[List]:
  def empty[A] = Nil
  def add[A](fa: List[A], a: A) = a :: fa

object OptionContainer extends Container[Option]:
  def empty[A] = None
  def add[A](fa: Option[A], a: A) = Some(a)

def fill[F[_]](c: Container[F], xs: Seq[Int]): F[Int] =
  xs.foldLeft(c.empty[Int])(c.add)

@main def run(): Unit =
  println(fill(ListContainer, Seq(1, 2, 3)))
  println(fill(OptionContainer, Seq(1, 2, 3)))
List(3, 2, 1)
Some(3)

fill is written once and works for any Container; F is inferred from the argument. Passing the wrong shape is a kind error:

//> using scala 3.10.0

trait Container[F[_]]

class Box[A]
class Two[A, B]

val ok: Container[Box] = new Container[Box] {}
val bad: Container[Two] = new Container[Two] {}
val bad2: Container[Int] = new Container[Int] {}
-- Error: b_err.scala:9:19
9 |val bad: Container[Two] = new Container[Two] {}
  |                   ^^^
  |          Type argument Two does not have the same kind as its bound [_$1]
-- Error: b_err.scala:10:20
10 |val bad2: Container[Int] = new Container[Int] {}
   |                    ^^^
   |        Type argument Int does not have the same kind as its bound [_$1]

Type lambdas

A constructor of one parameter, Either[String, _], can be obtained from a two-parameter one by fixing an argument. Scala has no syntax for partially applying a type constructor, so Scala 3 offers a type lambda: [X] =>> Either[String, X] is a type constructor of kind * -> *. The arrow is =>> (double-headed, to tell it from a function arrow =>); on the left are the parameters, on the right a type that may use them. A type lambda is written wherever a type constructor is expected, most often as the argument of a higher-kinded parameter or as the right-hand side of a type alias.

//> using scala 3.10.0

type Pair = [X] =>> (X, X)
type Named = [X] =>> Map[String, X]
type Const = [A, B] =>> A

trait Functor[F[_]]:
  extension [A](fa: F[A]) def map[B](f: A => B): F[B]

given Functor[List] with
  extension [A](fa: List[A]) def map[B](f: A => B): List[B] = fa.map(f)

given Functor[Pair] with
  extension [A](fa: (A, A)) def map[B](f: A => B): (B, B) = (f(fa._1), f(fa._2))

given [K]: Functor[[V] =>> Map[K, V]] with
  extension [A](fa: Map[K, A]) def map[B](f: A => B): Map[K, B] = fa.map((k, v) => (k, f(v)))

def double[F[_]: Functor](fa: F[Int]): F[Int] = fa.map(_ * 2)

@main def run(): Unit =
  val p: Pair[Int] = (1, 2)
  println(double(List(1, 2, 3)))
  println(double(p))
  println(double(Map("a" -> 10, "b" -> 20)))
  val n: Named[Int] = Map("x" -> 1)
  println(n)
  val c: Const[String, Int] = "const"
  println(c)
List(2, 4, 6)
(2,4)
Map(a -> 20, b -> 40)
Map(x -> 1)
const

Pair takes one type and gives a pair of it; Const ignores its second parameter. The Functor instance for maps is written with an inline lambda, [V] =>> Map[K, V], that fixes the key type. The given and extension forms are described on Givens & Using Clauses and Extension Methods.

Bounds, nesting and what is not allowed

A type lambda parameter may have bounds, and a lambda may return another lambda or take type constructors as parameters:

//> using scala 3.10.0

type Id = [X] =>> X
type Compose = [F[_], G[_]] =>> [X] =>> F[G[X]]
type ListOfOption = Compose[List, Option]
type Bounded = [X <: AnyVal] =>> List[X]

@main def run(): Unit =
  val a: Id[Int] = 3
  val b: ListOfOption[Int] = List(Some(1), None)
  val c: Bounded[Int] = List(1)
  println((a, b, c))
(3,List(Some(1), None),List(1))

A type argument that violates the bound is rejected where the lambda is applied:

//> using scala 3.10.0

type Bounded = [X <: AnyVal] =>> List[X]
val bad: Bounded[String] = List("a")
-- [E057] Type Mismatch Error: d_err.scala:4:17
4 |val bad: Bounded[String] = List("a")
  |                 ^
  |               Type argument String does not conform to upper bound AnyVal

Variance annotations are not allowed on a type-lambda parameter: [+X] =>> List[X] is rejected with "no +/- variance annotation allowed here" on 3.10.0. A type lambda is a type, not a class, so it cannot be instantiated.

Inference and partial unification

Scala 2.12 needed the compiler flag -Ypartial-unification for F[A] to unify with Either[String, Int] (F = Either[String, *], A = Int). Scala 3 does this unification by default, without a flag:

//> using scala 3.10.0

def head[F[_], A](fa: F[A]): String = fa.toString

@main def run(): Unit =
  println(head(Right(1): Either[String, Int]))
  println(head(Map("a" -> 1)))
  println(head((1, 2)))
Right(1)
Map(a -> 1)
(1,2)

When the type has more parameters than F takes, the compiler fixes the leading ones and abstracts over the last: Either[String, Int] gives F = [X] =>> Either[String, X] and Map[String, Int] gives F = [X] =>> Map[String, X]. To abstract over any other parameter, use a type lambda explicitly.

Kind polymorphism with AnyKind

Every type parameter normally has an upper bound Any, which restricts it to proper types of kind *. A parameter bounded by AnyKind accepts a type of any kind: Int, List, Either or a lambda taking a constructor.

//> using scala 3.10.0

trait Kinded[T <: AnyKind]:
  def name: String

given Kinded[Int] with
  def name = "Int (kind *)"
given Kinded[List] with
  def name = "List (kind * -> *)"
given Kinded[Either] with
  def name = "Either (kind * -> * -> *)"
given Kinded[[F[_]] =>> F[Int]] with
  def name = "[F[_]] =>> F[Int] (kind (* -> *) -> *)"

def describe[T <: AnyKind](using k: Kinded[T]): String = k.name

@main def run(): Unit =
  println(describe[Int])
  println(describe[List])
  println(describe[Either])
  println(describe[[F[_]] =>> F[Int]])
Int (kind *)
List (kind * -> *)
Either (kind * -> * -> *)
[F[_]] =>> F[Int] (kind (* -> *) -> *)

Because the kind of T is unknown, the language allows almost nothing to be done with it: it can be passed to other AnyKind parameters and used in type arguments, but a value of type T cannot exist, since T might not be a proper type.

//> using scala 3.10.0

def broken[T <: AnyKind]: Unit =
  val x: T = ???
  ()

def alsoBroken[T <: AnyKind](t: T): Unit = ()
-- [E056] Syntax Error: f.scala:4:9
4 |  val x: T = ???
  |         ^
  |         T cannot be used as a value type
  |
  |         where:    T is a type in method broken with bounds <: AnyKind
-- [E056] Syntax Error: f.scala:7:32
7 |def alsoBroken[T <: AnyKind](t: T): Unit = ()
  |                                ^
  |                                T cannot be used as a value type

AnyKind is a compiler-known type in the scala package that has no members and cannot be used as a value’s type. It is meant for library-level code that abstracts over types of arbitrary shape.

Migrating from kind-projector

Scala 2 had no syntax for a type lambda, and the kind-projector compiler plugin added one: Either[String, *] (or λ[x => Either[String, x]], Lambda[x => ...]), commonly written in libraries that needed Functor[Either[E, *]]. Scala 3.10.0 does not know the star form by default:

//> using scala 3.10.0

trait Functor[F[_]]

val x: Functor[Either[String, *]] = ???
-- [E006] Not Found Error: g_err.scala:5:30
5 |val x: Functor[Either[String, *]] = ???
  |                              ^
  |                              Not found: type *

There are two ways to migrate. The recommended one is to rewrite the lambdas with =>> as shown on this page. The other is to turn on the compatibility syntax with the compiler option -Xkind-projector; -Xkind-projector:underscores instead makes _ the placeholder, and then _ can no longer be used as a wildcard. The compiler help (-X) describes the option as allowing * "as type lambda placeholder to be compatible with kind projector", and it is off by default.

//> using scala 3.10.0
//> using option -Xkind-projector

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

given eitherFunctor: [E] => Functor[Either[E, *]]:
  def map[A, B](fa: Either[E, A])(f: A => B) = fa.map(f)

given Functor[Map[String, *]] with
  def map[A, B](fa: Map[String, A])(f: A => B) = fa.view.mapValues(f).toMap

@main def run(): Unit =
  println(summon[Functor[Either[String, *]]].map(Right(2): Either[String, Int])(_ + 1))
  println(summon[Functor[Map[String, *]]].map(Map("a" -> 1))(_ + 1))
  val x: Functor[λ[x => Either[Int, x]]] = eitherFunctor[Int]
  println(x.map(Left(1): Either[Int, Int])(_ + 1))
Right(3)
Map(a -> 2)
Left(1)

The conversion between the notations is mechanical:

kind-projector (Scala 2) Scala 3 type lambda

Either[String, *]

[X] =>> Either[String, X]

Map[K, *]

[V] =>> Map[K, V]

λ[x => (x, x)]

[X] =>> (X, X)

The old Scala 2 type-projection form ({ type L[X] = Either[String, X] })#L means the same lambda; it still compiles on 3.10.0, as noted on Dependent & Polymorphic Function Types.

Differences from Scala 2, Java and Kotlin

  • Scala 2. The F[_] parameter is identical. Type lambdas had to be written with the projection trick or a plugin, and partial unification needed -Ypartial-unification on 2.12 (on by default in 2.13).

  • Java. Generics cannot abstract over a type constructor: there is no way to write F<A> with F a parameter, which is why Java has Optional, Stream and CompletableFuture all defining their own map.

  • Kotlin. No higher-kinded types; libraries emulate them with an encoding such as a Kind<F, A> interface.

Pitfalls

  • Wildcard confusion. In F[_] the underscore declares an anonymous parameter; in a type argument List[_] is a wildcard (List[?] in Scala 3). The same symbol means different things in the two positions.

  • Wrong arity. A Functor[Either] is a kind error; use a lambda, Functor[[X] =>> Either[String, X]].

  • Kind errors are reported at the use. A Container[Two] error points at the type argument, not at the declaration of Container.

  • Nothing to do with a value of type T <: AnyKind. Add a separate type parameter, or use inline/match types, for the proper types you want values of.

  • No star placeholder. Without -Xkind-projector, Either[String, *] is a compile error.

Where the books are outdated

  • Scala 2 books present ({type L[A] = ...})#L and the kind-projector plugin as the only ways to write a partial application; =>> replaces both.

  • Books written before 2.13 describe -Ypartial-unification as something to remember to enable; it is the default now.

  • The Scala 3 AnyKind and kind-polymorphism feature is absent from Scala 2 books.