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. SincecallAsFunction
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."
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
.
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.