Selecting generic function type parameter from Any

Hi,

My question is:
Is there a way to call a generic function with its type parameter bound to the runtime type of an Any?

Why I want this?:

I'm trying to code something like Purescript's Exists type in Swift. Essentially I'm trying to write a type called Exists that holds a value of a type that is know when creating the Exists instance but it is immediately forgotten (kind of a type erasure). The idea is that you can never know what type is inside Exists, but if you "pass" a polymorphic function to an instance of Exists, the polymorphic function will be called with its generic parameter bound with the type hidden inside Exists.

Here is a simplified version of what I have:

func f<A>(_ a: A) -> String {
  return "\(a)"
}

public final class Exists {
    public init<A>(_ a: A) {
        self.a = a
    }
    let a: Any

    public func run() -> String {
        switch a {
        case let i as Int:
            return f(i)
        case let i as String:
            return f(i)
        default:
            fatalError()
        }
    }
}

let existsInt = Exists(0)
print(existsInt.run())
let existsString = Exists("Hello")
print(existsString.run())
let existsDouble = Exists(0.0)
print(existsDouble.run()) // Here we crash

I hold the type-erased value inside exists in a variable of type Any. I can then query the actual runtime type of the value in the Any inside the switch, but I need to manually specify each type I want to try. Notice how the return statement in each case is identical, since we just call the polymorphic function. Of course this is not a solution, because as soon as I create an Exists with a type that's not explicitly handled in the switch, the program crashes.

I don't care how hacky, ugly, or low level a solution is, as long as I don't need to rely on Objc. I was thinking maybe I can do something with Mirror, but I couldn't find anything that helped me.

Any help is appreciated.

There is no way to pass around generic functions as such. See the thread Passing around generic functions.

I'm aware of that. That's why in my simplified example I have hardcoded calls to f.
In the full scenario I pass the polymorphic functions around via a mechanism similar to that of Bow's FunctionK.

There's no runtime Any you can access from Swift. It's always treated as an instance of appropriate concrete type when you use any of the runtime queries, including when printing. Or do you mean?

public func run() -> String {
  return f(a)
}

My example was too simplified after all. You're right, I can just implement the run function above with f(a) without any casting and it works.

Here is another example, more close to my real code:

func f<A>(_ a: Wrapper<A>) -> String {
  return a.g()
}

public struct Wrapper<A> {
  let a: A

  func g() -> String {
    return "\(a)"
  }
}

public final class Exists {
    public init<A>(_ a: Wrapper<A>) {
        self.a = a
    }
    let a: Any
    // I can't say Wrapper<Any> here because generic types are generally invariant

    public func run() -> String {
        switch a {
        case let i as Wrapper<Int>:
            return f(i)
        case let i as Wrapper<String>:
            return f(i)
        default:
            fatalError()
        }
    }
}

let existsInt = Exists(Wrapper(a: 0))
print(existsInt.run())
let existsString = Exists(Wrapper(a: "Hello"))
print(existsString.run())
let existsDouble = Exists(Wrapper(a: 0.0))
print(existsDouble.run()) // Here we crash

Here, I can't implement run with f(a) because cannot convert value of type 'Any' to expected argument type 'Wrapper<_>'

If I could really call the generic function with the type inside the Any I would not have this problem.

Although it's true that I wouldn't have this problem either if I could cast Wrapper<A> to Wrapper<Any>.

So, do you know a way to achieve either one of this two things?

I see. The problem is as you said, that you (correctly) cannot cast Wrapper between ones with different generic parameters. Another one is that Swift doesn't have an unspecified type Wrapper<*>. I think the closest you could do is to use protocol:

private protocol FCallable { // a.k.a. F_Able
  func g() { ... }
}
extension Wrapper {
  func g() { ... }
}

...
switch a {
case let i as FCallable: f(i)
...
}

which might also work better since you can also conform other types, like Int if you want custom behaviour.

Thanks, this was good insight.

Unfortunately I've tried some different approaches using a protocol with no luck. I could make something work by using a protocol with associated types, but then I cannot use the protocol in the cast: case let i as FCallable

If I don't use associated types on the protocol, I can call something on the protocol, but I don't have a type parameter to be able to call the generic function with the right type.

How are generic functions functions implemented in swift? IIRC, there's a runtime representation a generic function that given the type information of the input "binds" its generic parameter to the type of the input.

Is there a way I can access this runtime representation of the generic function? Maybe some hidden api?

You can access the runtime type by using type(of:), however, due to how variables and typealiases are differently handled, you cannot use the result of type(of:) as a type parameter:

typealias MyInt1 = Int
let MyInt2 = type(of: 0)

MyInt1.self == MyInt2.self  // returns true

let a: [MyInt1] = []
let b: [MyInt2] = []  // error, MyInt2 is a variable, not a type
                      // it cannot be found in scope as a type

Here's a different way to type-erase which may be useful for your use case: if you need to access a's .g() method, you could store it in the Exists type itself when initializing the instance.

public struct Wrapper<A> {
  let a: A

  func g() -> String {
    return "\(a)"
  }
}

public final class Exists {
    let a: Any
    let g: () -> String
    
    public init<A>(_ a: Wrapper<A>) {
        self.a = a
        self.g = a.g
    }

    public func run() -> String {
        self.g()
    }
}

let existsInt = Exists(Wrapper(a: 0))
print(existsInt.run())
let existsString = Exists(Wrapper(a: "Hello"))
print(existsString.run())
let existsDouble = Exists(Wrapper(a: 0.0))
print(existsDouble.run())  // Here we don't crash anymore

In order to be able to use this approach, you need to move the content of the f function inside the .run() method.

Ok, I found a solution!

The trick is to specify the type parameter of the generic function while we still know the generic type of our Wrapper. This is what the function run in WrapperAndF<A> does. You construct WrapperAndF<A> with a Wrapper<A>, thus run knows how to call the generic function f.

This WrapperAndF<A> is then kept in Exists as its superclass SomeWrapperAndF, which hides the type parameter A. However, we can still call run()!

func f<A>(_ a: Wrapper<A>) -> String {
  return a.g()
}

public struct Wrapper<A> {
  let a: A

  func g() -> String {
    return "\(a)"
  }
}

public final class Exists {
    public init<A>(_ a: Wrapper<A>) {
        self.a = WrapperAndF(a)
    }
    let a: SomeWrapperAndF
    // I can't say Wrapper<Any> here because generic types are generally invariant

    public func run() -> String {
      return a.run()
    }
}

// In my real scenario, SomeWrapperAndF needs an additional type parameter.
// This needs to be a class because a protocol with an associated type F
// couldn't be stored inside Exists as an existential.
// We'll be able to use a protocol when this gets done:
// https://forums.swift.org/t/lifting-the-self-or-associated-type-constraint-on-existentials/18025
open class SomeWrapperAndF {
    open func run() -> String {
        fatalError()
    }
}

final class WrapperAndF<A>: SomeWrapperAndF {
    init(_ wrapper: Wrapper<A>) {
        self.wrapper = wrapper
    }

    let wrapper: Wrapper<A>

    public override func run() -> String {
        return f(wrapper)
    }
}

let existsInt = Exists(Wrapper(a: 0))
print(existsInt.run())
let existsString = Exists(Wrapper(a: "Hello"))
print(existsString.run())
let existsDouble = Exists(Wrapper(a: 0.0))
print(existsDouble.run()) // Here we crash

@xAlien95 Thank you very much! I didn't see your answer when writing mine but indeed it seems we arrived at the same core idea.

Here you can see the solution in it's actual context.

Basically I was able to implement an existential type for a value F<A> where the specific type parameter A is hidden. This works with Bow's Higher kinded types simulation.

Terms of Service

Privacy Policy

Cookie Policy