Passing around generic functions

I recently encountered a situation where I wanted to pass a generic function as an argument, while keeping it generic.

Here’s a toy example entirely unrelated to what I was actually doing:

// If we have this generic function
func foo<T: Numeric>(_ x: T) -> T {
  x + 1
}

// We can call it on many different types of values:
func bar() {
  foo(1)
  foo(2.0)
  foo(3 as UInt8)
}

// But if we want the caller to pass a function of their choice
// with the same signature as foo, how can we do it?
func hmm(_ f: (T)->T) {    // error: Use of undeclared type 'T'
  f(1)
  f(2.0)
  f(3 as UInt8)
}

There’s nowhere to hang a <T: Numeric> on the parameter f, and we don’t want to put it on hmm because then f could only operate on one type.

Is this something that other people have wanted?

• • •

Note that we can work around this by making the function a requirement of a protocol and passing a conforming type, so it’s clearly not a fundamental impossibility:

protocol P {
  static func f<T: Numeric>(_ x: T) -> T
}

func huh<T: P>(_ t: T.Type) {
  t.f(1)
  t.f(2.0)
  t.f(3 as UInt8)
}

But that makes the call-site require declaring a type, and then passing the type itself:

enum Doubler : P {
  static func f<T: Numeric>(_ x: T) -> T { 2 * x }
}

huh(Doubler.self)

Ideally I want to use trailing closures at the call-site:

hmm{ $0 * 2 }
8 Likes

I believe you're looking for higher-rank types (rank-2 for your use case). Friendlier explanation here.

A nice name for this in Swift might be "generic function values" or "generic closures". It could look like this:

func foo(_ f: <T> (T) -> T) {
       //  ^~~~~~~~~~~~~~~ first-class generic closure
  f(1)
  f(2.0)
  f(3 as UInt8)
}
let identity: <T> (T) -> T = { $0 }
foo(identity)

I don't think this language feature is widely supported outside of Haskell, as it's known to complicate type inference.

Though it is possible to emulate rank-2 types by adding a level of indirection, like defining a protocol with a generic method (as you did), or a ~> trait with a polymorphic apply method in Scala.

// `Rank2Function` represents `<T> (T) -> T`.
protocol Rank2Function {
  static func callAsFunction<T>(_ input: T) -> T
                         // ^~~ higher-rank type parameters bound here
}
func foo<Fn: Rank2Function>(_ f: Fn.Type) {
  // `static func callAsFunction` sugar isn't supported yet, so we have to spell it out.
  f.callAsFunction(1)
  f.callAsFunction(2.0)
  f.callAsFunction("Hello")
}
13 Likes

To add to Dan's excellent answer, this technique of using an apply function is called defunctionalization, in case you want to read up more on this. You may also see this pop up in other contexts; for example higher-kinded types can be mimicked in a language without higher-kinded types using defunctionalization. The Lightweight Higher-Kinded Polymorphism paper describes how this would work in the context of OCaml.

3 Likes