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
Source: Tour of Scala: Inner classes
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.
Source: Tour of Scala: Inner classes
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
applyand 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()andb.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.Waypointdoes not accept avalley.Waypoint. When a method must accept any, takeTrail#Waypointor move the class to a companion. -
Instance not stable. A method call cannot be written in a type (
val x: getTrail().Waypointis a syntax error): bind the result to avalfirst (val t = getTrail(), thent.Waypoint). -
Lost refinement. Assigning a value to a variable of the abstract type erases what the compiler knew: after
val s: Source = w,s.Itemis abstract. Keep the more precise type, or write the member in the signature, such asSource { 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-stylePolyobjects 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.