Type-erasing closures to `(Any, Any, ...) -> Any`?

Is it possible to type-erase a closure (e.g. (T, U) -> R for arbitrary concrete types) to something like (Any, Any) -> Any?

let function = (+) as (Float, Float) -> Float
let typeErased = function as! (Any, Any) -> Any
$ swift function-type-erasure.swift
function-type-erasure.swift:2:27: warning: cast from '(Float, Float) -> Float' to unrelated type '(Any, Any) -> Any' always fails
let typeErased = function as! (Any, Any) -> Any
                 ~~~~~~~~ ^   ~~~~~~~~~~~~~~~~~
Could not cast value of type '(Swift.Float, Swift.Float) -> Swift.Float' (0x117573730) to '(Any, Any) -> Any' (0x117573770).
Stack dump:
0.	Program arguments: /Applications/Xcode-beta12_3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -interpret function-type-erasure.swift -enable-objc-interop -stack-check -sdk /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -color-diagnostics -target-sdk-version 11.0 -module-name main
1.	Swift version 5.3-dev (LLVM 14a10d635b229fb, Swift c08d0804e2e44ac)
2.	While running user code "function-type-erasure.swift"
0  swift-frontend           0x00000001133a60f5 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 37
1  swift-frontend           0x00000001133a5365 llvm::sys::RunSignalHandlers() + 85
2  swift-frontend           0x00000001133a66c6 SignalHandler(int) + 262
3  libsystem_platform.dylib 0x00007fff6abaa5fd _sigtramp + 29
4  libsystem_platform.dylib 000000000000000000 _sigtramp + 18446603338725546528
5  libsystem_c.dylib        0x00007fff6aa80808 abort + 120
6  libswiftCore.dylib       0x000000011745c6e5 swift::fatalError(unsigned int, char const*, ...) + 149
7  libswiftCore.dylib       0x00000001174522b7 swift::swift_dynamicCastFailure(void const*, char const*, void const*, char const*, char const*) + 71
8  libswiftCore.dylib       0x000000011745232a swift::swift_dynamicCastFailure(swift::TargetMetadata<swift::InProcess> const*, swift::TargetMetadata<swift::InProcess> const*, char const*) + 106

Some clarifications and requirements:

  • Type-erasing to Any does not work for my use case. I need a type-erased type that is callable within Swift.
  • If it helps, my use case would be supported if input types are required to be thin function references (e.g. @convention(thin).

Some challenges:

  • Covariance and contravariance of function parameter and result types.
    • Maybe (Any, Any, ...) -> Never works as the destination type.

You can wrap your function in a new closure and perform the casting within the body of that function?

let function = (+) as (Float, Float) -> Float
let typeErased: (Any, Any) -> Any = { a, b in
   function(a as! Float, b as! Float) as Any
}
3 Likes

Genius, thank you. Happy Thanksgiving!

1 Like

Good work. You don't need that final cast though.

func erase<Parameter, Return>(
  _ ƒ: @escaping (Parameter, Parameter) -> Return,
  _: Parameter.Type = Parameter.self
) -> (Any, Any) -> Any {
  { ƒ($0 as! Parameter, $1 as! Parameter) }
}
erase(+, Float.self)
erase(+, Int.self)
1 Like

Nice. Here's what I ended up with:

func eraser<A0, A1, Result>(
  _ function: @escaping (A0, A1) -> Result
) -> (Any, Any) -> Any {
  return { any0, any1 in
    guard let a0 = any0 as? A0 else {
      fatalError()
    }
    guard let a1 = any1 as? A1 else {
      fatalError()
    }
    return function(a0, a1)
  }
}

(Variadic generics please?)

1 Like

Yeah, I know :slight_smile: That final explicit cast will always succeed and works implicitly just as well. But I thought it made the concept clearer as an example.

1 Like

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.

Here's what I ended up with: https://github.com/AnarchoSystems/DescriptiveFunctor/blob/main/Sources/DescriptiveFunctor/Executables.swift

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.

1 Like

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
2 Likes
Terms of Service

Privacy Policy

Cookie Policy