[Pitch] UUID v7, other improvements

To add to the wishlist pile: conversion functions to/from GUID on Windows would be nice. Or even bridging the types entirely.

I never understood why the property was called uuidString either, considering nearly all other types use description instead. So I'm also against renaming this property to string or anything other than description.

Introducing the functions uppercase() and lowercase() does make sense to me, as well as adding a new initializer to String that accepts an uppercase argument (or even just one of these solutions).

CustomStringConvertible places no constraints on the type's output, so the String value can be anything, since it's intended to be used with String(describing:) and print. You should never rely on the format of this value.

On the other hand, uuidString is guaranteed to be a valid UUID string value, always (despite what UUIDs in other languages may say about upper or lower casing).

They aren't the same.

3 Likes

As with any other protocol requirement, conforming types are free to provide guarantees for their concrete implementations over and above that of the protocol they're conforming to.

So, while true that you can't rely on the format of description in a generic context—and, indeed, it's advised by the documentation that you shouldn't call description directly at all in a generic context—you can absolutely rely on guarantees of concrete types.

(And, speaking of guarantees, LosslessStringConvertible?)

5 Likes

You can if the type also conforms to LosslessStringConvertible, which is what Swift's Foundation could have done when the protocol was first introduced. This is what the previously mentioned pitch was addressing.

1 Like

Yes, the greater guarantee makes that the most appropriate. It's unfortunate the two protocols are directly related, description has a bad history on Apple platforms.

The short answer is that we can provide a far more efficient implementation. First - we only need to create 1 string, instead of 2. Second - the implementation can create a lowercase string from the start, avoiding lookup tables into unicode data.

There is a benchmark in the pull request, as a reference.

7 Likes

Here is my line of thinking on the addition of a new lowercased String function, vs adding two new static functions or a converting initializer.

I tend to avoid deprecating API just for naming reasons. My opinion is that this creates "deprecation fatigue," where developers start to ignore warnings because they feel somewhat arbitrary. For example, if you have code which today uses uuidString, doesn't care about the case, and has no need for the new API - why would you need to switch to a new API? I prefer to leave deprecation for cases where something is actively harmful and we really do need people to move off it.

For similar reasons, we cannot change the behavior of the existing method. There exists plenty of code out there that would break if we change the result of the uuidString to be lowercased. (For those that may not be aware, this change will be present not just in the Foundation package but also on every Apple OS that ships Swift 6.4, in the shared Foundation library that exists there).

And finally, given that we have an existing method, we should give the new one a name that is nearby, for discoverability in autocomplete, documentation, etc.

3 Likes

All LosslessStringConvertible types (which UUID isn't yet) gain the initializer String(_: some LosslessStringConvertible) which returns a copy of the description.

Currently UUID's description is uppercase, so the uppercase parameter of this new constructor String(_: UUID, uppercase: Bool) can't default to false. Otherwise it won't be obvious which overload is called, and each of them will do something different.

2 Likes

Correct, implicit in my question would be whether it'd be feasible to make description lowercase as part of that work—but as correctly pointed out above, we've had this particular discussion before so it's probably not necessary to hash it out again.

Hi Tony,

I noticed in the proposal you chose to allow injecting a Date instead of a Clock. What is the reason for this? Should this be something to add to the 'Alternatives considered'?

Hi @maartene - the spec requires the use of a specific definition of elapsed time, what we would call UTCClock (see also this pitch). I don't think the use of other clocks would end with a correct UUID - or, if they did, it would simply be UTCClock in the first place. I can add more info on that to the proposal.

3 Likes

Thanks for clarifying. Yeah, maybe good to add to the 'alternatives considered'.

2 Likes

Big +1 from me!

A minor addition to the factory methods: should we link via SeeAlso directly to the relevant RFC sections for further reading?

Also, I quite like the initializers proposed here over UUID.version4(), which feels a bit clunky:

1 Like

I'm not entirely sold on the tricky solution here, but I do agree that using vX reads better than versionX, since versioned UUIDs are almost always written as "UUIDvX" in prose. So UUID.v7 or UUID(.v7, ...) would match user expectations more naturally.

7 Likes

Why even allow the syntax of UUID(.v5, customData: Data()) if it results in a compilation error? In my mind it simpler to just provide the factory method. And each version can have its own, since they may have different required arguments (this is noted in the proposal's alternatives considered).

7 Likes

They are valid, just not versioned, no? And for the purpose of deterministic testing we haven't had a problem with the current format. We have talked about going padded UUIDv4 in the next major version of the library:

UUID(1)
// 00000000-0004-0008-0000-000000000001

…but haven't wanted to introduce a breaking change for now. We could also imagine a v7-safe version of this.

From RFC 9562:

The variant field consists of a variable number of the most significant bits of octet 8 of the UUID.

[A "variant" of 0xxx]: Reserved. Network Computing System (NCS) backward compatibility, and includes the Nil UUID as per Section 5.9

Interoperability, in any form, with variants other than the one defined here [10xx] is not guaranteed but is not likely to be an issue in practice.

So while they're not banned, neither is there any formal ruling about whether they're valid or not, because the definers of such UUIDs are from Apollo NCS, which is long since defunct. Foundation should be able to represent such UUIDs, but I don't think it should encourage creating them.

EDIT: if you want to make custom UUIDs, v8 is here now, and when Foundation gets around to adding that I'd be much more supportive of a "v8 with a smaller integer in and the rest set to zeros".

1 Like

The point is that the compiler would prevent you from specifying that form for that version, while maintaining the init-style construction for all versions.

If we really want to go the factory method route, should we follow the API Design Guidelines and prefix them with make, ie. UUID.makeVersion7(…)?

2 Likes

That's "prohibiting"...

In addition to yours:

  1. so a single initializer would either need to accept many optional parameters
  1. or use an associated-value enum

I was merely suggesting the third way out, which is flexible and type safe.. As type safe as the static factory methods. That's if we want to provide a consistent way to make UUID's ("only via inits") instead of "via a mixture of inits + statics".

1 Like