Dependent & Polymorphic Function Types

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). Dependent function types and polymorphic function types were added in Scala 3.0, so all of it also works on the 3.9 LTS (3.9.0, 2026-09-03) and the 3.3 LTS. Path-dependent types and dependent methods are older and are already present in Scala 2.

In most languages the type of a method result is written with the types of its parameters, never with the values. Scala lets a type be tied to a particular value: the type valley.Waypoint is a different type from ridge.Waypoint because valley and ridge are different values. This page follows that idea in four steps: path-dependent types and abstract type members, methods whose result type mentions a parameter, the function types that describe such methods, and function values that are themselves generic. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails. For ordinary function values see Functions as Values; for generic methods see Generics & Bounds.

Inner classes and path-dependent types

A class defined inside another class is a member of every instance of the outer class. new on an inner class always happens through an instance, and the resulting type carries that instance as a path: ridge.Waypoint is the Waypoint of ridge.

//> using scala 3.10.0

class Trail(val name: String):
  class Waypoint(val label: String):
    override def toString = s"$label on $name"

@main def run(): Unit =
  val ridge = Trail("Ridge")
  val valley = Trail("Valley")
  val w1: ridge.Waypoint = ridge.Waypoint("summit")
  val w2 = valley.Waypoint("lake")
  println(w1)
  println(w2)
  val anyTrail: Trail#Waypoint = w1
  println(anyTrail)
summit on Ridge
lake on Valley
summit on Ridge

Trail#Waypoint is the type projection that means "a Waypoint of some Trail`"; every `ridge.Waypoint is a subtype of it, so the assignment above is accepted. A ridge.Waypoint, however, is not a valley.Waypoint:

//> using scala 3.10.0

class Trail(val name: String):
  class Waypoint(val label: String)

@main def run(): Unit =
  val ridge = Trail("Ridge")
  val valley = Trail("Valley")
  val w1: ridge.Waypoint = ridge.Waypoint("summit")
  val bad: valley.Waypoint = w1
-- [E007] Type Mismatch Error: dependent-and-polymorphic-function-types/a_err.scala:10:29
10 |  val bad: valley.Waypoint = w1
   |                             ^^
   |                             Found:    (w1 : ridge.Waypoint)
   |                             Required: valley.Waypoint

This is the point of path-dependent types: the compiler stops a waypoint of one trail from being attached to another trail, a mistake that would otherwise need a runtime check. The path must be stable (a val, an object, this, or a chain of those); a var or a method call cannot appear in a type.

Abstract type members

A trait or class can declare a type without defining it, and subclasses fix it. Together with a path-dependent reference this lets one member’s type follow another’s: given a Source, the type s.Item is whatever item type that particular s chose.

//> using scala 3.10.0

trait Source:
  type Item
  def items: List[Item]

class Words extends Source:
  type Item = String
  def items = List("ridge", "lake")

def firstItem(s: Source): s.Item = s.items.head

def pair(s: Source): (s.Item, Int) = (s.items.head, s.items.size)

@main def run(): Unit =
  val w = Words()
  val i: String = firstItem(w)
  println(i)
  println(pair(w))
  val erased: Source = w
  val x = firstItem(erased)
  println(x)
ridge
(ridge,2)
ridge

firstItem(w) has type w.Item, which the compiler knows to be String, so it can be assigned to a String without a cast. When the argument is only known as a Source, the result is the abstract erased.Item and no more can be said about it. An abstract type member is the object-oriented counterpart of a type parameter: Source[Item] would work too, but a member keeps the type out of every signature that only mentions the trait, and it supports the family-of-types pattern (several related types that must be chosen together) without a long list of type arguments.

Dependent methods

A method whose result type (or a later parameter type) refers to an earlier parameter is a dependent method. firstItem above is one: its result is s.Item, and pair shows the dependency inside a tuple. The parameter is a value, not a type: def f[B <: Source](b: B): B#Item would be a different and weaker thing, and is rejected, as the section on type projection shows.

A very common dependent method is one that selects a type-level key:

//> using scala 3.10.0

trait Key:
  type Value
  def default: Value

object NameKey extends Key:
  type Value = String
  def default = "unnamed"

object LengthKey extends Key:
  type Value = Int
  def default = 0

def defaultOf(k: Key): k.Value = k.default

@main def run(): Unit =
  val s: String = defaultOf(NameKey)
  val n: Int = defaultOf(LengthKey)
  println(s"$s $n")
unnamed 0

The same method written with a type parameter would need two, Key and its value type, and callers would have to supply both.

Dependent function types

Until Scala 3, a dependent method could not be turned into a function value: the function type Key => ??? has no way to name the parameter in its own result. Scala 3 adds dependent function types, written by naming the parameter: (k: Key) => k.Value. They are the function-value counterpart of dependent methods and are a subtype of Function1[Key, Key#Value] in the usual sense.

//> using scala 3.10.0

trait Key:
  type Value
  def default: Value

object NameKey extends Key:
  type Value = String
  def default = "unnamed"

object LengthKey extends Key:
  type Value = Int
  def default = 0

def defaultOf(k: Key): k.Value = k.default

val dependentFn: (k: Key) => k.Value = k => k.default
val eta: (k: Key) => k.Value = defaultOf

@main def run(): Unit =
  val s: String = dependentFn(NameKey)
  println(s)
  println(dependentFn(LengthKey) + 1)
  val t: String = eta(NameKey)
  println(t)
unnamed
1
unnamed

The call dependentFn(LengthKey) + 1 type-checks because the result is LengthKey.Value, known to be Int. A method is eta-expanded to a dependent function value when the expected type is a dependent function type, as eta shows (without an expected type, val f = defaultOf is also given a dependent function type on 3.10.0). A function value of a plain type such as Key => Key#Value would lose the dependency. The parameter name in the type is used only for the dependency; it need not match the name of the parameter of the lambda.

Polymorphic function types

An ordinary method can have type parameters but a function value cannot: A => A has a fixed A. A polymorphic function type puts the type parameters in front of the function arrow, [A] => A => A, and a polymorphic function literal binds them with the same syntax, [A] => (a: A) => a. The value stays generic after it has been stored in a val or passed as an argument.

//> using scala 3.10.0

val firstOf: [A] => (xs: List[A]) => Option[A] = [A] => (xs: List[A]) => xs.headOption

val twice = [A] => (f: A => A, a: A) => f(f(a))

def applyAll(f: [A] => List[A] => List[A]): (List[Int], List[String]) =
  (f(List(1, 2, 3)), f(List("a", "b", "c")))

@main def run(): Unit =
  println(firstOf(List(1, 2)))
  println(firstOf(List("x")))
  println(twice[Int](_ + 3, 1))
  println(applyAll([A] => (xs: List[A]) => xs.reverse))
  println(applyAll([A] => (xs: List[A]) => xs.take(2)))
Some(1)
Some(x)
7
(List(3, 2, 1),List(c, b, a))
(List(1, 2),List(a, b))

firstOf is applied to a list of Int and then to a list of String; a function of type List[Int] => Option[Int] could do only one of those. applyAll is the useful case, a method that takes a function and uses it at several types. This capability is called rank-2 (or higher-rank) polymorphism; in Scala 2 it needed a trait with a polymorphic apply method:

trait ListFunction:
  def apply[A](xs: List[A]): List[A]

A monomorphic function does not satisfy the parameter type, and the compiler says so:

//> using scala 3.10.0

def applyAll(f: [A] => List[A] => List[A]): (List[Int], List[String]) =
  (f(List(1, 2, 3)), f(List("a", "b", "c")))

@main def run(): Unit =
  val mono: List[Int] => List[Int] = _.reverse
  println(applyAll(mono))
-- [E007] Type Mismatch Error: c_err.scala:8:19
8 |  println(applyAll(mono))
  |                   ^^^^
  |                   Found:    (mono : List[Int] => List[Int])
  |                   Required: [A] => (x$1: List[A]) => List[A]

A polymorphic function can take several type parameters ([A, B] => (A, B) => (B, A)) and can be curried like any function. Bounds on its type parameters are written as for a method, [A <: AnyVal] => ....

Context parameters in polymorphic functions

A polymorphic function cannot have a using parameter list, because a lambda has no parameter lists of that kind. The result type can be a context function, Ordering[A] ?=> List[A], which is how a polymorphic function receives a given:

//> using scala 3.10.0

val sortCtx: [A] => List[A] => Ordering[A] ?=> List[A] =
  [A] => (xs: List[A]) => (ord: Ordering[A]) ?=> xs.sorted

@main def run(): Unit =
  println(sortCtx(List(3, 1, 2)))
  println(sortCtx(List("b", "a")))
List(1, 2, 3)
List(a, b)

The Ordering is resolved at each call from the element type, exactly as for a method with a context bound. Writing (using ord: Ordering[A]) => ... after the type parameters is a syntax error on 3.10.0 ("expression expected but : found"). Context functions are covered on Context Functions.

Type projection and its limits

Outer#Inner is a type projection. Scala 2 allowed projecting from any type, which made the type system unsound; Scala 3 allows it only on a class or other concrete type. On a type that is merely a bound, it is rejected:

//> using scala 3.10.0

trait Box:
  type T

def g[B <: Box](x: B#T): Unit = ()
-- Error: d2_err.scala:6:19
6 |def g[B <: Box](x: B#T): Unit = ()
  |                   ^
  |                   B is not a legal path
  |                   since it is not a concrete type

Two practical consequences follow. First, Trail#Waypoint (projection on a class) and Box#T (projection on a trait whose member is abstract) compile, but a Box#T tells you almost nothing: it is "the T of some Box`", and values of it cannot be related to any particular box. Second, the Scala 2 type-lambda idiom `({ type L[X] = Either[String, X] })#L still compiles on 3.10.0 (the compiler special-cases a projection on a refinement that only defines a type), but it is superseded by =>>; see Type Lambdas & Higher Kinds. Prefer a path-dependent type (s.Item, with s a parameter) or a type parameter over a projection whenever the instance is available.

Differences from Scala 2, Java and Kotlin

  • Scala 2. Path-dependent types and dependent methods exist in Scala 2 (dependent methods since 2.10). Dependent function types, polymorphic function types and polymorphic function literals do not; the workaround is a trait with a polymorphic apply and an anonymous class (new ListFunction { def apply[A](xs: List[A]) = ... }). General type projection is unsound and restricted in Scala 3.

  • Java. An inner (non-static) class has a hidden reference to the outer instance, but the type does not record which instance: a.new Inner() and b.new Inner() are the same Java type. Java has no value-dependent types and no polymorphic lambda; a generic method is the only way to be polymorphic, and it cannot be stored in a variable.

  • Kotlin. Inner classes behave like Java’s, with no path-dependent typing. There are no polymorphic function values or dependent function types.

Pitfalls

  • Different instances, different types. Two objects created from the same constructor produce incompatible inner types; a method that accepts ridge.Waypoint does not accept a valley.Waypoint. When a method must accept any, take Trail#Waypoint or move the class to a companion.

  • Instance not stable. A method call cannot be written in a type (val x: getTrail().Waypoint is a syntax error): bind the result to a val first (val t = getTrail(), then t.Waypoint).

  • Lost refinement. Assigning a value to a variable of the abstract type erases what the compiler knew: after val s: Source = w, s.Item is abstract. Keep the more precise type, or write the member in the signature, such as Source { type Item = String }.

  • Type inference for polymorphic literals. A parameter type is required in [A] => (xs: List[A]) => ... unless the expected type provides it.

Where the books are outdated

  • Scala 2 books present rank-2 polymorphism through a trait with a generic apply, shapeless-style Poly objects or ~> natural transformations; polymorphic function types replace the hand-written forms.

  • Books that use the type-projection trick (({type L[A] = ...})#L) to get a type lambda are describing a workaround that Scala 3 keeps only for compatibility; the =>> syntax replaces it.

  • Dependent function types were not in Scala 2.13 and are missing from most books written before 2021.