dan-zheng
(Dan Zheng)
1
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
1 Like
dan-zheng
(Dan Zheng)
2
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.
sveinhal
(Svein Halvor Halvorsen)
3
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
dan-zheng
(Dan Zheng)
4
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
dan-zheng
(Dan Zheng)
6
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
sveinhal
(Svein Halvor Halvorsen)
7
Yeah, I know
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
Anachron
(Markus Kasperczyk)
8
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: DescriptiveFunctor/Executables.swift at main · AnarchoSystems/DescriptiveFunctor · GitHub
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
pyrtsa
(Pyry Jahkola)
9
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