Swift 4 emulating Decoder behaviour


(Joanna Carter) #1

Greetings

I notice that, with Swift 4, I can decode an object like this :

  • let retrievedSpot = try decoder.decode(ParkingSpot.self, from: retrievedData)

And that will return a ParkingSpot, as expected, into retrievedSpot.

However, I thought I would try the same technique with one of my pet projects…

I have a protocol and an implementing struct :

  • public protocol PropertyProtocol
  • {
  • static var propertyType: Any.Type { get }
  •
  • var untypedValue: Any? { get }
  • }
  •
  • public struct Property<valueT : DefaultValueProvider> : PropertyProtocol
  • {
  • public static var propertyType: Any.Type
  • {
  • return valueT.self
  • }
  •
  • public var untypedValue: Any?
  • {
  • return value
  • }
  •
  • public var value = valueT()
  • }

Now, I want to be able to use a "factory" method to create an instance of Property<T>, bound to its parameter type. So, I followed the same principal as Decoder :

  • struct PropertyFactory
  • {
  • static func createProperty<T>(_ type: T.Type) -> PropertyProtocol where T : DefaultValueProvider
  • {
  • return type.createProperty()
  • }
  • }

DefaultValueProvider is defined as follows and String is extended to conform to it :

  • public protocol DefaultValueProvider
  • {
  • init()
  • }
  •
  • extension String : DefaultValueProvider { }

Now, this works fine if I pass a known type :

  • let pproperty = PropertyFactory.createProperty(String.self)

But, if I hold the type to be passed in in an Any.Type or DefaultValueProvider.Type variable, then doing this :

  • let type: Any.Type = String.self
  •
  • if let propertyType = type as? DefaultValueProvider.Type
  • {
  • let p = PropertyFactory.createProperty(propertyType)
  •
  • …

Fails to compile with the message : Cannot invoke 'createProperty' with an argument list of type '(DefaultValueProvider.Type)'

Likewise Decoder.decode(…) will not accept storing the type in an Any.Type or Decodable.Type variable.

I find this odd and perplexing. Is this a known issue or has nobody yet realised that this could be useful ?

Joanna

···

--
Joanna Carter
Carter Consulting


(Itai Ferber) #2

Hi Joanna,

Your example doesn’t work for a few reasons:

1. You’re getting a compiler error because of the difference between the representation of metatypes (`Foo.Type` for some type `Foo`) of concrete types (e.g. `String`, `Int`), and protocols (e.g. `DefaultValueProvider`). Some of the compiler folks can correct my exact use of terminology here, but the essence is this: when you `as?`-cast a type to a concrete type (e.g. `type as? String.self`), you’ll get a concrete metatype which can be used dynamically as you would a concrete type elsewhere. When you `as?`-cast a type to a protocol type (`type as? DefaultValueProvider`), however, you get a protocol-constrained metatype which is not concrete and cannot be used dynamically as you would statically. You can call statically-known protocol methods on the metatype (e.g. `(type as! DefaultValueProvider).init()`), but the concrete type is not known. You can see this with a small example:

   protocol P {
       init()
   }

   extension Int : P {}

   let type: Any.Type = Int.self
   if let specific = type as? P.Type {
       // type of specific.init() is P, not Int
       print(specific.init())
   }

   This, then coincides with:

2. The fact that methods generic on types can only take concrete type parameters. Protocol metatypes cannot be passed in as you would a concrete metatype:

   protocol P {}
   extension Int : P {}

   func foo<T>(_ type: T.Type) {
       print(type)
   }

   let type: Any.Type = Int.self
   foo(type) // cannot invoke 'foo' with an argument list of type '(Any.Type)'
   foo(type as! P.Type) // cannot invoke 'foo' with an argument list of type '(P.Type)'
   foo(type as! Int.Type) // works just fine

   Arguments to generic methods _must_ be statically knowable for the method to be dispatched; otherwise you’ll get the compiler error that you see.

This is also why you can’t `decode(Decodable.self)` or `decode(Any.self)` — those are not concrete types.
(On a side note, though, this would never be possible with the `Decodable` protocol. We need a concrete type to decode because values are otherwise ambiguous. Is `5` an `Int8`, `UInt8`, …, `Int`, `UInt`, …, `Int64`, `UInt64`, `Float`, or `Double`? Is `"2017-07-14"` a `String` or a `Date`? What would you expect to get for `decode(Decodable.self)`?)

— Itai

···

On 13 Jul 2017, at 11:46, Joanna Carter via swift-users wrote:

Greetings

I notice that, with Swift 4, I can decode an object like this :

  • let retrievedSpot = try decoder.decode(ParkingSpot.self, from: retrievedData)

And that will return a ParkingSpot, as expected, into retrievedSpot.

However, I thought I would try the same technique with one of my pet projects…

I have a protocol and an implementing struct :

  • public protocol PropertyProtocol
  • {
  • static var propertyType: Any.Type { get }
  •
  • var untypedValue: Any? { get }
  • }
  •
  • public struct Property<valueT : DefaultValueProvider> : PropertyProtocol
  • {
  • public static var propertyType: Any.Type
  • {
  • return valueT.self
  • }
  •
  • public var untypedValue: Any?
  • {
  • return value
  • }
  •
  • public var value = valueT()
  • }

Now, I want to be able to use a "factory" method to create an instance of Property<T>, bound to its parameter type. So, I followed the same principal as Decoder :

  • struct PropertyFactory
  • {
  • static func createProperty<T>(_ type: T.Type) -> PropertyProtocol where T : DefaultValueProvider
  • {
  • return type.createProperty()
  • }
  • }

DefaultValueProvider is defined as follows and String is extended to conform to it :

  • public protocol DefaultValueProvider
  • {
  • init()
  • }
  •
  • extension String : DefaultValueProvider { }

Now, this works fine if I pass a known type :

  • let pproperty = PropertyFactory.createProperty(String.self)

But, if I hold the type to be passed in in an Any.Type or DefaultValueProvider.Type variable, then doing this :

  • let type: Any.Type = String.self
  •
  • if let propertyType = type as? DefaultValueProvider.Type
  • {
  • let p = PropertyFactory.createProperty(propertyType)
  •
  • …

Fails to compile with the message : Cannot invoke 'createProperty' with an argument list of type '(DefaultValueProvider.Type)'

Likewise Decoder.decode(…) will not accept storing the type in an Any.Type or Decodable.Type variable.

I find this odd and perplexing. Is this a known issue or has nobody yet realised that this could be useful ?

Joanna

--
Joanna Carter
Carter Consulting
_______________________________________________
swift-users mailing list
swift-users@swift.org
https://lists.swift.org/mailman/listinfo/swift-users


(Joanna Carter) #3

Hi Itai

  • You’re getting a compiler error because of the difference between the representation of metatypes (Foo.Type for some type Foo) of concrete types (e.g. String, Int), and protocols (e.g. DefaultValueProvider). Some of the compiler folks can correct my exact use of terminology here, but the essence is this: when you as?-cast a type to a concrete type (e.g. type as? String.self), you’ll get a concrete metatype which can be used dynamically as you would a concrete type elsewhere. When you as?-cast a type to a protocol type (type as? DefaultValueProvider), however, you get a protocol-constrained metatype which is not concrete and cannot be used dynamically as you would statically. You can call statically-known protocol methods on the metatype (e.g. (type as! DefaultValueProvider).init()), but the concrete type is not known.

Unfortunately, I am only too aware of this limitation :relieved:

Essentially, what I am trying to achieve is to extend the functionality of "any" type by wrapping it in a generic type that contains the functionality.

I have a perfectly valid, and very useful, hierarchy that provides, not only change observation but also validation of properties when an attempt is made to change their value. It also provides for encapsulation of all the property values in an object for storage or transmission.

The essentials are :

BaseObject
—PropertyBag
——[Property]

The BaseObject holds the PropertyBag and forwards all calls to property setters to the PropertyBag, which contains a subscript, indexed on the property name that accesses the relevant Property.

The Property contains metadata about e.g. : whether the property is readonly, a validation hook and a means of observing the change if it is valid.

In C#, it was easy to access the metadata of a type instead of having to create an instance of that type to get to it, as in Swift. Not having complete reflection is definitely a gaping hole in Swift.

All I really wanted, in this instance, is to be able to bind a Property<T> to the metatype of T, as in C#'s MakeGenericType([typeArgs]) method thus

// pseudocode
{
  let metatype = type(of: aType)
  
  for propertyType in metatype.propertyTypes
  {
    let property = type(of: Property<>).makeGenericType([propertyType])
    
    propertyBag.append(property)
  }
}

Once the generic properties are "in the bag", they are accessed and manipulated using the Visitor design pattern, thus allowing a heterogeneous collection of properties that are all treated correctly, according to their bound type.

In brief, I wanted a heterogeneous collection of generic types, all of which implement a non-generic protocol ; something that is perfectly possible now.

However, the "automatic" creation of the generic types into the non-generic protocol, based on the parameter type, seems to be unattainable ; unless I create a protocol that contains a factory method but that has then to be adopted by each individual type I wish to wrap in the generic type ; which is messy :wink:

Joanna

···

--
Joanna Carter
Carter Consulting