Skip to content

Instantly share code, notes, and snippets.

@cdmcmahon
Last active February 25, 2020 23:15
Show Gist options
  • Save cdmcmahon/1048ae668d00735e6844a17cb298ab15 to your computer and use it in GitHub Desktop.
Save cdmcmahon/1048ae668d00735e6844a17cb298ab15 to your computer and use it in GitHub Desktop.
Swift Evolution Pitch: Implicit conversion of callable values to functions

Hello. This is just a pitch, no implementation. SE-0253: Callable values of user-defined nominal types called out the implicit conversion of callable values to functions in its future directions.

A value cannot be implicitly converted to a function when the destination function type matches the type of the  callAsFunction  method. Since  callAsFunction  methods are normal methods, you can refer to them directly via  .callAsFunction  and get a function.

Implicit conversions impact the entire type system and require runtime support to work with dynamic casts; thus, further exploration is necessary for a formal proposal. This base proposal is self-contained; incremental proposals involving conversion can come later.

As an interested party I'd like to kick off that "further exploration."

Example Motivation

There is frequently good reason to wrap a basic function type into a value type. It can then be given static properties/methods, instance methods, etc. I have illustrated this with a simple example of a Predicate<T> type to wrap functions of (T) -> Bool.

struct Predicate<T> {
    let test: (T) -> Bool
}

extension Predicate {
    func and(_ other: Predicate<T>) -> Predicate<T> {
        return Predicate { val in
            return self.test(val) && other.test(val)
        }
    }
}

let isEven = Predicate { $0 % 2 == 0 }
let isGreaterThanFive = Predicate { $0 > 5 }
let isEvenAndGreaterThanFive = isEven.and(isGreaterThanFive)

Implicit conversion would allow greater interoperability of such types with other libraries. Specifically, that if a function argument expects a shape, e.g. (T) -> Bool, a callable value with the shape (T) -> Bool should be passable. In that case we make the following change:

struct Predicate<T> {
    let test: (T) -> Bool

    func callAsFunction(_ t: T) -> Bool {
        return test(t)
    }
}
// accepts Predicate<T> without extending Array, because callAsFunction can
// implicitly cast to `(T) -> Bool`
myArray.filter(where: isEvenAndGreaterThanFive) 

The ability for all callable values to be converted to functions allows them all to interact seamlessly. In fact, the and function above would likely be re-written to same a simple (T) -> Bool so it could accept either a predicate-shaped lambda or a Predicate.

Prior art

Java's @FunctionalInterface annotation provides an example of the power of this in another language. Lambdas and classes using the @FunctionalInterface can interact seamlessly and be integrated with other libraries that know nothing about local functional interfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment