Static/Class stored properties in generic types?

I'm attempted to create an observables system that can be updated and subscribed to generically. I use something like this:

final class Observer<Value> { }

protocol Observable {
    associated type Value
    static var observable: Observer<Value>
}

And I can use it fine with normal types:

struct Model: Observable {
    static let observable = Observer<Model>()
}

But attempting to make generic types conform:

final class Future<Value> {
    static let observable = Observer<Future<Value>>()
}

leads to a simple error: Static stored properties not supported in generic types

Is this a limitation that will be lifted eventually? Any ideas for what I can do in the meantime? Ideally, I'd like to have an API like this:

Model.observerable.observe { }
Future<Model>.observable.observe { }

Otherwise, I'm stuck creating properties for every type of generic I want to observe. Considering I'm using this for networking and other async source, that would be quite a few, so it's not really scalable without some general support. Ideas?

2 Likes

I think a good part of this restriction came from the fact that it's unclear whether Foo<X>.value and Foo<Y>.value share the same storage. There's actually only one answer that works, but it's still non-obvious enough that we weren't sure if it was a good idea to allow it.

On the implementation side, it's also non-trivial to implement, since new generic types can be created ad infinitum at runtime.

I don't recall any discussions about whether it's a good idea to support; the lack of implementation is the only barrier I know of.

It is possible to implement static stored properties on generic types manually using a global dictionary – albeit with a bit of boilerplate, see:

1 Like

From the user's perspective, defining a static constant in a generic is a common scenario and should be well supported.

3 Likes

Since globals are already lazily-initialized, I think the only question here is whether there's value in also supporting the truly-unique case — which would have to be semantically restricted to not refer to the type parameters, of course — in addition to the generic case. But I suppose it's pretty clear from precedent elsewhere that static doesn't mean the former, so yeah, it's just a matter of implementation.

It seems to me we could potentially hide the true uniqueness behind the ABI (at least when it isn't exposed as a stored property with @_fixed_layout) and treat it as an optimization. If the property is a let and its initializer independent of the type context, we could collapse it into a unique variable.

2 Likes

Makes sense.

Would that work for reference holding types?
That could be another limitation on the optimization.

I'd like to replicate this pattern:

extension Notification.Name {
  static let myCustomNotification = Notification.Name(...)
}

NotificationCenter.default.post(name: .myCustomNotification)

That is, to be able to define a static let on a type, so I can use the dot-shorthand at call-site.

I'd like to define a generic version of Notification.Name that is generic over a phantom type, so I can do this:

struct TypedNotification<A> {
  let name: Notification.Name
}

extension NotificationCenter {
  func post<A>(_ note: TypedNotification<A>, object: A) { ... }
  func addObserver<A>(_ note: TypedNotification<A>, handler: (A) -> Void) { ... }
}

extension TypedNotification {
  // error: Static stored properties not supported in generic types
  static let myCustomTypedNotification = TypedNotification<CustomPayloadType>(name: ...)
}

NotificationCenter.default.addObserver(.myCustomTypedNotification) { payload in
  // payload has correct type
}

I also encountered a use for this recently, but I didn’t want to bump the thread. Since it’s already bumped though…

While playing around with some numerical things, I wrote a memoized, recursive, O(log(n)) Fibonacci function using the relations:

F2n = (Fn-1 + Fn+1) ¡ Fn
F2n+1 = Fn2 + Fn+12

First I implemented it concretely for Int:

Fibonacci for Int
private var fibonacciCache: [Int: Int] = [0: 0, 1: 1]

func fibonacci(_ x: Int) -> Int {
  if let y = fibonacciCache[x] { return y }
  precondition(x >= 0)
  
  let h = x/2
  let a = fibonacci(h-1)
  let b = fibonacci(h)
  let c = a+b
  
  let y = x.isEven ? b*(a+c) : b*b + c*c
  fibonacciCache[x] = y
  return y
}

let x = fibonacci(92)
print(x)    // 7540113804746346429

Then I decided to make it generic over BinaryInteger. My first thought was to store the cache as a static property of a generic type:

struct FibonacciCache<T: BinaryInteger> {
  static var cache: [T: T] = [0: 0, 1: 1]
}

But of course that doesn’t work because static stored properties are not supported in generic types.

I ended up hacking together a really ugly workaround, which to spare everyone’s sensibilities I will refrain from posting. Let’s just say it involves [ObjectIdentifier: Any] and a lot of casting. :-)

1 Like

Class stored properties not supported in generic types

I have encountered this issue recently, I try to implement a type-erased container of one of my protocol that have multiple static properties.

protocol Custom {
  static var name: String { get }
  var tabBarName: String? { get }
}

struct AnyCustom<Inner : Custom> : Custom {
  let value : Inner
  
  static var name: String = Inner.name
  var tabBarName: String? { get { self.value.tabBarName } }
  
  init(_ value : Inner) {
    self.value = value
  }
}

It's not clear from the response above if it will be supported or not. I there a way to go around it ?

In a case like this you should use a computed property:

static var name: String { get { Inner.name } }

It seems a bit strange that type-erased container would want to use a stored property.

3 Likes

Thanks,

Generic type-associated stored properties seems like a useful feature. AA tree algorithms use a sentinel node (a type associated property) to avoid constantly checking for null.

1 Like

I just recently came across a need for this and am bumping to show interest in support for it.

1 Like

Bumping to support again as I've just came across this issue. It's a pretty unexpected restriction.