Dependent Function Types - More Details

Initial implementation in PR #3464.

Syntax

FunArgTypes       ::=  InfixType
                    |  ‘(’ [ FunArgType {',' FunArgType } ] ‘)’
                    |  ‘(’ TypedFunParam {',' TypedFunParam } ‘)’
TypedFunParam     ::=  id ‘:’ Type

Dependent function types associate to the right, e.g. (s: S) => (t: T) => U is the same as (s: S) => ((t: T) => U).

Implementation

Dependent function types are shorthands for class types that define apply methods with a dependent result type. Dependent function types desugar to refinement types of scala.FunctionN. A dependent function type (x1: K1, ..., xN: KN) => R of arity N translates to:

FunctionN[K1, ..., Kn, R']:
   def apply(x1: K1, ..., xN: KN): R

where the result type parameter R' is the least upper approximation of the precise result type R without any reference to value parameters x1, ..., xN.

The syntax and semantics of anonymous dependent functions is identical to the one of regular functions. Eta expansion is naturally generalized to produce dependent function types for methods with dependent result types.

Dependent functions can be implicit, and generalize to arity N > 22 in the same way that other functions do, see the corresponding documentation.

Examples

The example below defines a trait C and the two dependent function types DF and IDF and prints the results of the respective function applications:

trait C { type M; val m: M }

type DF = (x: C) => x.M

type IDF = (x: C) ?=> x.M

@main def test =
   val c = new C { type M = Int; val m = 3 }

   val depfun: DF = (x: C) => x.m
   val t = depfun(c)
   println(s"t=$t")   // prints "t=3"

   val idepfun: IDF = summon[C].m
   val u = idepfun(using c)
   println(s"u=$u")   // prints "u=3"

In the following example the depend type f.Eff refers to the effect type CanThrow:

trait Effect

// Type X => Y
abstract class Fun[-X, +Y]:
   type Eff <: Effect
   def apply(x: X): Eff ?=> Y

class CanThrow extends Effect
class CanIO extends Effect

given ct: CanThrow = new CanThrow
given ci: CanIO = new CanIO

class I2S extends Fun[Int, String]:
   type Eff = CanThrow
   def apply(x: Int) = x.toString

class S2I extends Fun[String, Int]:
   type Eff = CanIO
   def apply(x: String) = x.length

// def map(f: A => B)(xs: List[A]): List[B]
def map[A, B](f: Fun[A, B])(xs: List[A]): f.Eff ?=> List[B] =
   xs.map(f.apply)

// def mapFn[A, B]: (A => B) -> List[A] -> List[B]
def mapFn[A, B]: (f: Fun[A, B]) => List[A] => f.Eff ?=> List[B] =
   f => xs => map(f)(xs)

// def compose(f: A => B)(g: B => C)(x: A): C
def compose[A, B, C](f: Fun[A, B])(g: Fun[B, C])(x: A):
   f.Eff ?=> g.Eff ?=> C =
   g(f(x))

// def composeFn: (A => B) -> (B => C) -> A -> C
def composeFn[A, B, C]:
   (f: Fun[A, B]) => (g: Fun[B, C]) => A => f.Eff ?=> g.Eff ?=> C =
   f => g => x => compose(f)(g)(x)

@main def test =
   val i2s = new I2S
   val s2i = new S2I

   assert(mapFn(i2s)(List(1, 2, 3)).mkString == "123")
   assert(composeFn(i2s)(s2i)(22) == 2)

Type Checking

After desugaring no additional typing rules are required for dependent function types.