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 |
|---|---|---|
|
|
a proper type; values exist |
|
|
takes one type, gives a proper type |
|
|
takes two types |
|
|
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)
Source: Scala 3 Reference: Type Lambdas
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]
Source: Scala 3 Reference: Type Lambdas
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.
Source: Scala 3 Reference: Type Lambdas
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))
Source: Scala 3 Reference: Type Lambdas
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.
Source: Scala 3 Reference: Type Lambdas
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.
Source: Scala 3 Reference: Type Lambdas
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 |
|---|---|
|
|
|
|
|
|
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-unificationon 2.12 (on by default in 2.13). -
Java. Generics cannot abstract over a type constructor: there is no way to write
F<A>withFa parameter, which is why Java hasOptional,StreamandCompletableFutureall defining their ownmap. -
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 argumentList[_]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 ofContainer. -
Nothing to do with a value of type
T <: AnyKind. Add a separate type parameter, or useinline/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] = ...})#Land the kind-projector plugin as the only ways to write a partial application;=>>replaces both. -
Books written before 2.13 describe
-Ypartial-unificationas something to remember to enable; it is the default now. -
The Scala 3
AnyKindand kind-polymorphism feature is absent from Scala 2 books.