Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a good alternative for static stored properties of generic types in swift?

Since static stored properties are not (yet) supported for generic types in swift, I wonder what is a good alternative.

My specific use-case is that I want to build an ORM in swift. I have an Entity protocol which has an associatedtype for the primary key, since some entities will have an integer as their id and some will have a string etc. So that makes the Entity protocol generic.

Now I also have an EntityCollection<T: Entity> type, which manages collections of entities and as you can see it is also generic. The goal of EntityCollection is that it lets you use collections of entities as if they were normal arrays without having to be aware that there's a database behind it. EntityCollection will take care of querying and caching and being as optimized as possible.

I wanted to use static properties on the EntityCollection to store all the entities that have already been fetched from the database. So that if two separate instances of EntityCollection want to fetch the same entity from the database, the database will be queried only once.

Do you guys have any idea how else I could achieve that?

like image 951
Evert Avatar asked Jun 22 '16 09:06

Evert


People also ask

What is static property in Swift?

Swift lets you create properties and methods that belong to a type, rather than to instances of a type. This is helpful for organizing your data meaningfully by storing shared data. Swift calls these shared properties “static properties”, and you create one just by using the static keyword.

Which of the following are generic types in Swift?

Swift 4 language provides 'Generic' features to write flexible and reusable functions and types. Generics are used to avoid duplication and to provide abstraction. Swift 4 standard libraries are built with generics code. Swift 4s 'Arrays' and 'Dictionary' types belong to generic collections.

What is the use of static in Swift?

Swift allows us to use a static prefix on methods and properties to associate them with the type that they're declared on rather than the instance. We can also use static properties to create singletons of our objects which, as you have probably heard before is a huge anti-pattern.

What is generic constraints in Swift?

Generic Where Clauses Type constraints, as described in Type Constraints, enable you to define requirements on the type parameters associated with a generic function, subscript, or type. It can also be useful to define requirements for associated types. You do this by defining a generic where clause.


Video Answer


1 Answers

The reason that Swift doesn't currently support static stored properties on generic types is that separate property storage would be required for each specialisation of the generic placeholder(s) – there's more discussion of this in this Q&A.

We can however implement this ourselves with a global dictionary (remember that static properties are nothing more than global properties namespaced to a given type). There are a few obstacles to overcome in doing this though.

The first obstacle is that we need a key type. Ideally this would be the metatype value for the generic placeholder(s) of the type; however metatypes can't currently conform to protocols, and so therefore aren't Hashable. To fix this, we can build a wrapper:

/// Hashable wrapper for any metatype value. struct AnyHashableMetatype : Hashable {    static func ==(lhs: AnyHashableMetatype, rhs: AnyHashableMetatype) -> Bool {     return lhs.base == rhs.base   }    let base: Any.Type    init(_ base: Any.Type) {     self.base = base   }    func hash(into hasher: inout Hasher) {     hasher.combine(ObjectIdentifier(base))   }   // Pre Swift 4.2:   // var hashValue: Int { return ObjectIdentifier(base).hashValue } } 

The second is that each value of the dictionary can be a different type; fortunately that can be easily solved by just erasing to Any and casting back when we need to.

So here's what that would look like:

protocol Entity {   associatedtype PrimaryKey }  struct Foo : Entity {   typealias PrimaryKey = String }  struct Bar : Entity {   typealias PrimaryKey = Int }  // Make sure this is in a seperate file along with EntityCollection in order to // maintain the invariant that the metatype used for the key describes the // element type of the array value. fileprivate var _loadedEntities = [AnyHashableMetatype: Any]()  struct EntityCollection<T : Entity> {    static var loadedEntities: [T] {     get {       return _loadedEntities[AnyHashableMetatype(T.self), default: []] as! [T]     }     set {       _loadedEntities[AnyHashableMetatype(T.self)] = newValue     }   }    // ... }  EntityCollection<Foo>.loadedEntities += [Foo(), Foo()] EntityCollection<Bar>.loadedEntities.append(Bar())  print(EntityCollection<Foo>.loadedEntities) // [Foo(), Foo()] print(EntityCollection<Bar>.loadedEntities) // [Bar()] 

We are able to maintain the invariant that the metatype used for the key describes the element type of the array value through the implementation of loadedEntities, as we only store a [T] value for a T.self key.


There is a potential performance issue here however from using a getter and setter; the array values will suffer from copying on mutation (mutating calls the getter to get a temporary array, that array is mutated and then the setter is called).

(hopefully we get generalised addressors soon...)

Depending on whether this is a performance concern, you could implement a static method to perform in-place mutation of the array values:

func with<T, R>(   _ value: inout T, _ mutations: (inout T) throws -> R ) rethrows -> R {   return try mutations(&value) }  extension EntityCollection {    static func withLoadedEntities<R>(     _ body: (inout [T]) throws -> R   ) rethrows -> R {     return try with(&_loadedEntities) { dict -> R in       let key = AnyHashableMetatype(T.self)       var entities = (dict.removeValue(forKey: key) ?? []) as! [T]       defer {         dict.updateValue(entities, forKey: key)       }       return try body(&entities)     }   } }  EntityCollection<Foo>.withLoadedEntities { entities in   entities += [Foo(), Foo()] // in-place mutation of the array } 

There's quite a bit going on here, let's unpack it a bit:

  • We first remove the array from the dictionary (if it exists).
  • We then apply the mutations to the array. As it's now uniquely referenced (no longer present in the dictionary), it can be mutated in-place.
  • We then put the mutated array back in the dictionary (using defer so we can neatly return from body and then put the array back).

We're using with(_:_:) here in order to ensure we have write access to _loadedEntities throughout the entirety of withLoadedEntities(_:) to ensure that Swift catches exclusive access violations like this:

EntityCollection<Foo>.withLoadedEntities { entities in   entities += [Foo(), Foo()]   EntityCollection<Foo>.withLoadedEntities { print($0) } // crash! } 
like image 89
Hamish Avatar answered Oct 12 '22 11:10

Hamish