[Pre-Pitch] SwiftAwake()

It's still possible, but nobody has done anything about it.

Ok. What are the outstanding parts of the design which would need to be figured out in order to make a formal proposal?

Well, if we assume that there's an attribute you put on a protocol that makes its conformances dynamically enumerable, I can see a few basic design questions:

  • What's the name of the attribute?
  • How do you enumerate the conformances of a protocol in code?
  • Are there ordering guarantees in that enumeration?
  • What happens to generic types that conform to the protocol?
  • Does the enumeration include subclasses of classes that conform to the protocol?
  • Does the enumeration include non-public types?
  • Is there a way to opt out of the enumeration?
  • On systems that allow code to be dynamically loaded, should there be some way to get notified of the existence of future types that conform to the protocol?
8 Likes

…also:

• How are types that conditionally conform handled?

6 Likes

Thanks. I will think on these further, but here is my first stab. (Anyone) Feel free to point out any mistakes or things I am missing:

My thought is to have a special protocol inheritance (similar to CaseIterable). When a protocol or class inherits from this special protocol, the compiler generates a static function on the type returning all conformers/subclasses as an array. If a static function is impossible (I know we can't add them manually to protocols right now... but the compiler might have a special way to do this), then we could use a free function as suggested above.

Looking at how Sourcery handles things like this as inspiration, we should probably return some sort of reflection object/struct rather than the types directly. This object should have the type as one of it's properties, but also a way to pick apart any nested generics, etc... that it has. But if that is too difficult to implement, we could just return an array of Types, and then have the function take some options about what it returns (e.g. Do we want all the specializations of generic types returned or just the base type?).

  • What's the name of the attribute?

I've been using ConformerIterable, which I kind of hate. Open to suggestions here...

  • How do you enumerate the conformances of a protocol in code?

See above. A static func allConforming(options:) on the actual protocol if possible, or a free function typesConforming(to: options:) if not.

  • Are there ordering guarantees in that enumeration?

It should be stable, but other than that no specific ordering. If you want an ordering, you can sort the resulting list in some way (most likely using a static function defined in the protocol).

  • What happens to generic types that conform to the protocol?

If we are returning a reflection object, we could potentially group the specializations into a single object, and they can then be iterated over if desired, or just seen as the single base type as desired. If we are returning types directly, then we should have an option to include all conforming specializations or just the base generic type. By default, I think all conforming specializations should be in the list.

(As an optimization, the list of specializations for a generic object could be a separate function call on the reflection object. That way, the information is only given if requested.)

• How are types that conditionally conform handled?

Only specializations that conform should be included

  • Does the enumeration include subclasses of classes that conform to the protocol?

See above about generics, but in general, I would say yes, since they all conform to the protocol. Again they could be grouped together either by option or as a returned reflection object. I think the default should be not to group them though.

  • Does the enumeration include non-public types?

This is an interesting question. I think it should include all visible types from where the function is called, at least by default. If this is part of the information in the reflection object, then it would be easy to filter by access level.

  • Is there a way to opt out of the enumeration?

I don't think so. Let's say we don't supply a built-in way to opt out, but someone really needs it. They could just add a static var/func to the protocol which lets them filter the list based on the result.

  • On systems that allow code to be dynamically loaded, should there be some way to get notified of the existence of future types that conform to the protocol?

Ideally, I think the function should give you all the types that are currently loaded. If new code has been dynamically loaded, you can call the function again, and it would include the new types as well. If we choose to notify, that could be part of an add-on proposal later.

Thoughts? Suggestions? Problems?

As I think about what I wanted this for (serialization), I wonder if it would be enough (or a good first-pass.. or something else entirely) to just have a magical protocol or attribute that basically makes name mangling for that type publicly available. In other words, all it provides is a mechanism to get a String that can then any time later be turned back into the very same Type (and only that type). This would eliminate any need to worry about things like iterating all possible types, defining an ordering, worrying about conditional conformance, etc, etc.

So this might not be the best way to go about it, but imagine like:

@identifiableType class Foo {}
@identifiableType struct Bar {}
struct Baz {}

let fooTypeString = String(identifierFor: Foo.self)
let barTypeString = String(identifierFor: Bar.self)
let bazTypeString = String(identifierFor: Baz.self) // error

// some free function, maybe, named something like:
func type(for identifier: String) -> Any.Type?

// so then you can do this:
let fooType = type(for: fooTypeString) // == Foo.self
let bazType = type(for: bazTypeString) // == nil

And the idea there would be that if you passed the type(for:) function any string that doesn't exactly conform to the mangled name of a known @identifiableType, it would return nil even if the mangling is exactly correct for an existing type. Likewise, the String(identifierFor:) initializer would, perhaps ideally, not even compile if you attempt to pass a type isn't @identifiableType. (Alternatively it could be a failable initializer and potentially return nil maybe.)

This is obviously quite a lot different from iterable types or SwiftAwake()... :stuck_out_tongue:

edit:

I still feel like I want this, but while I was writing this up, I got a reply in another thread that does a pretty convincing job of explaining why this might not be a good idea at all: Why is it possible to get a class from a String but not a struct or enum?

1 Like

That is quite likely an infinite set of types.

It's tempting to try to conservatively approximate it with "the conforming specializations that are actually used in the program", but I wouldn't want to do that for several reasons:

  • First, the used set is still potentially infinite, because generic code can dynamically use new specializations. This wouldn't be possible if Swift required generic code to be statically monomorphizable, but it doesn't.

  • Second, the used set is sensitive to the presence of dead code. Programmers who innocently remove an unused function could remove the last "use" of a particular specialization and break program behavior. This is highly undesirable.

  • Third, the converse of the point above: programmers who want to force a specialization to appear in the enumeration may need to introduce a spurious unused function.

  • Finally, maintaining the used set introduces a very significant amount of overhead for a corner-case feature. The program would have to dynamically remember every specialization of a type that's "used", just in case that type ends up conforming to an enumerable protocol (which could happen in a different module), and even if the "use" is just a temporary abstraction that is optimized away.

All of this makes me wonder whether it would be better to have a more explicit feature for building static tables of values where the individual entries are distributed throughout the program, something like:

collection options: String  // `options` is a value of `GlobalCollection<String>`

extension options { // probably not a good idea to re-use `extension` for this
  "-lm"             // these expressions all have to be values of type `String`
  "-I/usr/local/include"
}

extension options {
  "-Wall"
  "-Werror"
}

Array(options) // ["-lm", "-I/usr/local/include", "-Wall", "-Werror"], although the order wouldn't be guaranteed
1 Like

Another way to spin this idea might be to allow collections as user-defined attributes:

@attributeCollection var Handler: [URLHandler.Type]

// Adds Foo.self to the content of `Handler`
@Handler
struct RootHandler: URLHandler {
   ...
}
2 Likes

Interesting. And then we synthesize a getter for Handler that builds the array?

I'm still not really sold on making this type-declaration-centric, though. Making it an attribute answers the private types question, but it still leaves generics as a problem.

Yeah. Maybe as a stretch, we could use didSet to represent the "dynamic linker loaded more members" case.

On the other hand, if the feature is based around attributes rather than protocol conformance, it's easier to say up front that generic types aren't applicable. It'd also be possible to apply registration attributes to other kinds of decls, like individual functions or properties, which might make more sense for things like test discovery.

1 Like

Oh, that's cute.

Sure, but that just feels really limiting if the whole thing is built around type enumeration.

Being able to annotate functions to collect them definitely feels useful to me. Maybe we can have a "primitive" feature based on building global tables, and then user-defined attributes can hook into that to add entries per application?

What about representing an unspecialized generic type with a stand-in “type builder”? Here’s a quick mock-up:

enum ArrayKind {
  static func specialize<T>(_ type: T.Type) -> [T].Type {
    return [T].self
  }
}

let GenericArrayType = ArrayKind.self
let ArrayOfInt = GenericArrayType.specialize(Int.self)
assert(ArrayOfInt == [Int].self)

I wrote that as static on an enum, but it could just as easily be an instantiable struct.

The idea of the conforming-types enumeration is that you get a collection of P.Type; I don’t see any way that the stand-in type could conform.

Maybe we could also allow specific instantiations of generic types to be added to the list in an ad-hoc way, with something like your extension Options syntax. Putting the attribute on a concrete type then just becomes sugar for the common case of wanting to declare a type then register it in a list.

1 Like

Alternatively, if user-defined attributes can generally trigger adding entries to global collections, we can just make user-defined type attributes that add Self to a global collection of metatypes, which we can presumably make fail in some straightforward way when Self is dependent. And then the primitive notion remains "we can build a global table" rather than "we can build a global table specifically of types".

1 Like

This looks really similar to a feature I was wishing for a while ago. I've had a case where I wanted to associate values with types, and I tried using a global dictionary keyed by types only to have execution time dominated by Hasher. In that case the association was known at compile time, so it would have beer really nice if the compiler could have been able to do the mapping statically rather than requiring a hash lookup at runtime. For instance something like this (to piggy-back off your placeholder syntax):

map typeValues: [Type:Int]

extension typeValues {
    Foo: 1
    Bar: 2
}

typeValues[Foo.self]  // compiler can evaluate this to 1

func myFunc(type: Type) -> Int {
    return typeValues[type] // lookup can happen at runtime if the type is not statically-knowable
}

You can do that specific kind of association with a protocol:

protocol HasValue {
  static var value: Int { get }
}

extension String: HasValue {
  static let value = 1
}

func myFunc(type: HasValue.Type) -> Int {
  return type.value
}

myFunc(type: String.self)   // 1

Except for generic types. :frowning:

struct MyType<T> {
	var property: T
}

extension MyType: HasValue {
	static let value = 1  // Static stored properties not supported in generic types
}

Well, you can't use a let right now, but you can do static var value: Int { 1 }.

Oh, sure - although as soon as value needs to be get set, then you're back to being stuck again. :slight_smile:

Terms of Service

Privacy Policy

Cookie Policy