Binding generic types using a metatype parameter - Success!

Almost two years ago, I started a thread on the old mailing list - [swift-evolution] Binding generic types using a metatype parameter about trying to dynamically bind a generic type's parameter to any other type.

Well, I finally found a way!!!

What follows only works for generic types with a single parameter but it could easily be extended.

public protocol GenericType
{
  associatedtype ParameterType
  
  init()
}


public struct GenericFactory
{
  public static func boundGenericType<valueT, genericT : GenericType>(genericType: genericT.Type, valueType: valueT.Type) -> genericT.Type where genericT.ParameterType == valueT
  {
    return genericT.self
  }
}

An implementing type can look like this :

public class Property<valueType> : GenericType
{
  public typealias ParameterType = valueType
  
  public var value: valueType?
  
  public required init() { }
}

And the calling code looks like this :

  {
    let boundPropertyType = GenericFactory.boundGenericType(genericType: Property.self, valueType: String.self)
    
    let property = boundPropertyType.init()
    
    property.value = "Joanna"
  }

Please be kind enough to comment, good or bad :sunglasses:

In fact, it works just fine with as many parameters as you like :

public protocol TwoParameterGenericType
{
  associatedtype FirstParameterType
  
  associatedtype SecondParameterType
  
  init()
}


public class TestClass<firstT, secondT> : TwoParameterGenericType
{
  public typealias FirstParameterType = firstT
  
  public typealias SecondParameterType = secondT
  
  public required init() { }
}


public struct GenericFactory
{
  public static func boundGenericType<genericT : GenericType, valueT>(genericType: genericT.Type, valueType: valueT.Type) -> genericT.Type where genericT.ParameterType == valueT
  {
    return genericT.self
  }
  
  public static func boundGenericType<genericT : TwoParameterGenericType, firstT, secondT>(genericType: genericT.Type, firstType: firstT.Type, secondType: secondT.Type) -> genericT.Type
    where genericT.FirstParameterType == firstT,
    genericT.SecondParameterType == secondT
  {
    return genericT.self
  }
}

And the test code :

  {
    let boundType = GenericFactory.boundGenericType(genericType: TestClass.self, firstType: String.self, secondType: Int.self)
    
    let testObject = boundType.init()
  }

It still seems a bit clunky but it's a whole lot better than having to conform all possible parameter types to a protocol, as in the effort from two years ago.

Now, do you think this warrants a proposal to somehow include this behaviour in Foundation or some such?

I'm not sure this is quite doing what you expect or want: type inference is disguising that this static, not dynamic. Writing Property.self means the parameter is has to be inferred (which happens at compile time), it doesn't mean that it is the "parameterised type" Property where the parameter can be filled in later. One way to see this is:

class Property<valueType> {}
let _ = Property.self

This code has an error with some notes:

dynamic.swift:2:9: error: generic parameter 'valueType' could not be inferred
let _ = Property.self
        ^
dynamic.swift:1:7: note: 'valueType' declared as parameter to type 'Property'
class Property<valueType> {}
      ^
dynamic.swift:2:9: note: explicitly specify the generic arguments to fix this issue
let _ = Property.self
        ^
                <Any>

That is saying that the valueType parameter needs to be specified (because it can't be inferred, unlike your original example). There isn't a way to have a generic type without its parameters in Swift at the moment: the syntax means that it looks like it works sometimes, but this isn't quite true, it is type inference filling in the missing parameters.

Summarising all of this, the calling code is currently equivalent (in terms of dynamic versus static behaviour) to:

let boundPropertyType = Property<String>.self

let property = boundPropertyType.init()
    
property.value = "Joanna"

As another example for how this doesn't work dynamically, try to fill in the following function:

func stringOrInt<genericType: GenericType>(container: genericType.Type) -> Any {
    if Bool.random() {
        // make a genericType with ParameterType Int (e.g. Property<Int>) and return it
        return /* genericType<Int> */.init() 
    } else {
        // otherwise, have ParameterType String (e.g. Property<String>) 
        return /* genericType<String> */.init()
    }
}

Could you say a bit more about why you want to do this? Maybe there's another technique that works well for solving your problem in Swift but requires understanding the bigger picture.

I am looking to use reflection to provide a list of property types for a given class or struct, in order to create a "factory" that will contain a list or dictionary of bound property types, to avoid having to bind them every time before instantiating a property.

First, I need to make the types Hashable :

struct HashablePropertyType : Hashable
{
  let type: PropertyProtocol.Type
  
  init(type: PropertyProtocol.Type)
  {
    self.type = type
  }
  
  var hashValue: Int
  {
    return ObjectIdentifier(type).hashValue
  }
  
  static func == (lhs: HashablePropertyType, rhs: HashablePropertyType) -> Bool
  {
    return lhs.hashValue == rhs.hashValue
  }
}

Then I can use code like this :

    let boundPropertyType = GenericFactory.boundGenericType(genericType: Property.self, valueType: String.self)

    let property = boundPropertyType.init()

    var propertyTypes = [HashablePropertyType]()
    
    propertyTypes.append(HashablePropertyType(type: boundPropertyType))

    let propertyType = types[0]
    
    var newProperty = propertyType.type.init()
1 Like

I've been playing with this "factory" for a while now and found it to be reliable and it meets my needs.

However, although obviously it works, I'm curious to find out how it works.

Even though I do not have a class explicitly named Property (non-generic), I am able to pass Property.self without the need to pass a parameter type. Somewhere in my brain, something keeps telling me I've stumbled across something that is undocumented.

Or have I just not found the docs for it yet?

Ah, I think I've solved it.

public class Property<valueType> : GenericType
{
  public typealias ParameterType = valueType
  
  public var value: valueType?
  
  public required init() { }
}

It's the fact of binding the protocol's associated type to the class's generic parameter type that tells the compiler that the generic parameter type is known and, therefore, doesn't need explicitly naming.

Or is that just speculation? :thinking:

The parameter type is still there, it is just being inferred as I mentioned earlier. For instance:

struct Property<valueType> {
    let x: valueType
}
let x: Property = Property(x: 0)
let () = x

This has the following error at compile time:

infer.swift:5:10: error: 'Property<Int>' is not convertible to '()'
let () = x
         ^

As you can see, the type of x is Property<Int>, and since the error message happens at compile time, that indicates that the compiler statically knows that valueType is Int: it is implicitly filling that parameter in by inference. The code above is exactly the same as let x: Property<Int> = Property(0).

After a brief look, I can't find any documentation on this particular aspect.

Yeah, that's pretty much correct. Specifying the associated type like that means that the valueType parameter becomes equal to the valueT parameter of boundGenericType via the genericT.ParameterType == valueT in the where clause. This allows the inference to deduce the Property's generic parameter from that valueType function parameter.

So, all this got me thinking more about inference and I found out how to save time typing :grinning:

Now that's what I call inference