Recently had a similar problem, and in my case, I wanted to be able to chain my erased functions but fail ahead of call time if there's a type missmatch. The usecase was that I wanted to store the heterogenous closures in a private dictionary only accessible through phantom types giving me back the correct types.
The solution also minimizes typecasts, i.e. I can chain my functions in a fully safe way and only on return of the last function in the chain I use a downcast.
TLDR:
protocol Erased{
func tryChain(with other: Erased) throws -> Erased
}
struct ErasedExecutable<T> : Erased{
let wrapped : Executable<T, Any>
func tryChain(with other: Erased) throws -> Erased {
try wrapped.chain(other)
}
}
struct Executable<S,T> {
let closure : (S) -> T
let chain: (ErasedArrow) throws -> ErasedArrow
}
extension Executable {
init(closure: @escaping (S) -> T){
self.closure = closure
self.chain = {erased in
guard let chainable = erased as? ErasedExecutable<T> else{
//throw some error
}
return Executable<S,Any>{
chainable.wrapped.closure(closure($0))
}.erased()
}
func erased() -> ErasedExecutable<S> {
///pass self.chain to retain the type information!
ErasedExecutable(wrapped: Executable(closure: self.closure,
chain: self.chain))
}
}
It is now possible to safely chain erased executables - with the small caveat that you need to chain them in reverse order because you only retain type information on the input type.
Edit: revisiting this topic, I could probably have factored self.chain into a method, as the erased arrow already has the information on the input type. However, in the actual repo that I referenced, I also store dynamic type information on the return type to make an additional check if I can safely downcast to a given return type. This works because the initializer passing that type information is internal.
Back to the original question, I think the reason you can't arbitrarily cast (T, U) -> V to (Any, Any) -> Any is because function arguments are in the contravariant position where the conversion goes the other way than in the covariant case of, say, function result types: the type restricts what the function can be passed to.
Thus, if the original function is happy to consume a supertype as its argument instead there is a straightforward conversion using just as:
func foo(a: Any, b: Any) -> Int {
(a as? Int ?? 0) + (b as? Int ?? 0)
}
let typeErased = foo as (Int, Int) -> Any
print(typeErased(10, 20)) //=> 30