Getting the (more) fully qualified name of a type

Consider this exampe:

class One {
    enum State { case state }
}

class Two {
    enum State { case state }
}

func take<Value>(value: Value) {
    print("\(Value.self)")
}

take(value: One.State.state)
take(value: Two.State.state)

Currently this prints State for both. Changing it to to type(of: value) also prints State for both. It's only once you go to String(reflecting: Value.self) that you get the full type names: <lldb>.One.State, <lldb>.Two.State. Any reason why these are different? It seems surprising to me that the first two don't print the full type name, and that the third prints something different altogether.

String(describing:) prints a reasonably displayable form of the type name. String(reflecting:) preserves a lot more information to uniquely identify the type. Right now, the former doesn't include qualification and the latter does. This is behavior we could change, but we might need to be pretty careful about changing because people could be depending on it.

If you did change it would you at least continue to return a string that uniquely identifies the type from String(reflecting:)? There are some interesting things that are possible when you can get a unique string that is stable for the life of the process from a type (such as using it as the label of a queue).

1 Like

I guess my request here would be for the naming to be more intuitive. There's nothing that implies some of the methods get shorter vs. longer type names. I'd also question whether nested types like One.State should ever be shortened to just State, though I guess not doing so might conflict with being able to use just State when inside the One type. In any event, some predictability here would probably be a good thing.

This is concerning. I don't think functionality should belong to String, but rather, I think we should make special fields on meta type objects: MyClass.self.name, MyClass.self.module, MyClass.self.parentType, etc.

4 Likes

Yes, this sounds much more predictable than what we have now. A full set of properties would be more flexible than deciding which String overloads should exist, or what free functions could be used.

There are two problems with magic fields: first, that classes already have properties (static var name: String), and second, that you can't actually do anything with that information. What are you really trying to do?™

EDIT: Reflection features are good. We can put some of this stuff on Mirror or whatever replaces it, or maybe something specific to a class. But it's unclear what you actually want to do with the name of a type.

Some application and libraries want to turn types into strings (think database / http resource / serialization, etc.). struct Foo -> "Foo" -> SELECT * FROM Foo, GET /Foo/1.json, etc...

Today, this is not always easy, due to the multiple places a type can be defined (top-level, nested, inside a method call, etc.), which are handled in various ways by String(describing:):

func f() {
    struct Foo { }
    String(describing: Foo.self) // "Foo #1"
}

This "Foo #1" string is so weird that one just has to conclude that String(describing:) is still brittle, and not stabilized yet, despite what you wrote above:

Edit: I didn't really mean that String(describing:) is brittle. After all, it may not be the correct tool for the job. I just wanted to give a use case, and expose the difficulty implementing it.

Sure, that's a reasonable use case…but it's unclear how you want it to behave for a nested type, or a generic type, or a local type. And what works for your app may not work for someone else's. Still, if we think something's definitely an improvement we can make it (sorry for being negative, Jon and Alexander).

By the way, here's the explanation of "Foo #1", at least in Swift 4.1:

func show(_ first: Bool) {
  let ty: Any.Type
  if first {
    struct Foo {}
    ty = Foo.self
  } else {
    struct Foo {}
    ty = Foo.self
  }
  print(String(describing: ty))
  print(String(reflecting: ty))
}
show(false)
show(true)

In Swift 4.2 we changed how we distinguish these types, and now they're back to both printing as "Foo" in the simple case. (@Joe_Groff would be able to talk more about that.) I guess that shows that String(describing:) isn't that stable.

In 4.2, String(reflecting:) still shows a fully qualified name for local types, but the local type context is anonymized and printed in a way that's intended to be clear is non-stable, since no matter what reflection support we add in the future, it'd be a bad idea to incidentally rely on the presence or identity of local types in dynamic code.

3 Likes

@jrose What needs to happen in order for expected behavior to be defined? You had enumerated a list of behaviors for nested types, generic types, and local types. If the community were to somehow come to a consensus on those cases, could we move forward?