[Pitch] UUID v7, other improvements

This is a common misunderstanding, but this isn't a factory method. Factory methods aren't just any methods that create and return a thing—the factory method design pattern is specifically about creating objects without specifying their exact concrete type, such as a superclass method that's implemented by subclasses to return different kinds of objects.

In this case, we're calling an API on the concrete type UUID to return new values of that same exact type, and the way we spell that in Swift is with an initializer.

3 Likes

Using UUID(.v5, over UUID.v5( doesn't feel like an improvement, especially when it introduces a bunch of useless types that only exist as syntactic sugar. I also can't find any precedent for this syntax in the Foundation or standard library.

5 Likes

Not quite the case here (†) but still worth mentioning:
inits are generally better compared to static methods in case of subclassing (†). Which reminds me to ask about Objective-C angle - is this feature going to be backported there and if so in what form?

Subclassing example:

@interface MyURL: NSURL
@property NSInteger info;
@end

@implementation MyURL
-(instancetype)initWithString:(NSString*)string info:(NSInteger)info {
    self = [super initWithString:string];
    // we'd be in trouble if this was only available via [MyURL URLWithString:string]
    self.info = info;
    return self;
}
@end

void ctest(void) {
    MyURL* u = [[MyURL alloc] initWithString:@"12345678" info:123];
    NSLog(@"%ld", (long)u.info); // 123
}
1 Like
  1. so a single initializer would either need to accept many optional parameters
  2. or use an associated-value enum

Another option for the init with no sleight of hand involved:

extension UUID {
    enum Version {
        ...
        case v5(namespace: String, name: String)
        case v7
        case v8(Data)
    }
    init(_ version: Version) {
        // TODO
    }
}

Usage example:

UUID(.v5(namespace: "", name: ""))
UUID(.v8(Data()))
UUID(.v7)

Possibly along with the version property:

switch uuid.version {
    case .v8(let data): ...
    ...
}

and perhaps another one to get version number: let x: Int = uuid.versionNumber

2 Likes

Can you explain why that's better than the almost identical use site with fewer nested brackets?

UUID.v5(namespace: "", name: "")
UUID.v8(Data())
UUID.v7()
10 Likes

For value types (where subclassing concern is not applicable) mainly for consistency: "all UUIDs are constructed through init" vs "some versions use init and others using static methods." I suppose that was the original motivation of considering going with init() in "Alternatives considered".

Yeah, “consistency” is a red herring—enums are value types, and they are instantiated with this call site syntax. And I’m satisfied that UUID versions are sufficiently in the same vein as enum cases.

3 Likes

Yeah. There is plenty of precedent for constructing types with static methods when doing so makes more sense at the call site.

3 Likes

New to Swift, rusty at programming - since Swift seems to have that target audience very much in mind, I’ll go ahead and weigh in here. I like the original proposal to use .timeOrdered and offer version information if desired. The implication is that .timeOrdered and .random might change to different versions if better versions are available, so this is not the same thing as calling it .v7. Just imagine me hacking my way through a simple app, doing research for almost every step I take. “.”, I type, and wait hopefully for a list of options that obviously has what I want… For one thing, no, .v4, .v6, .v7 is definitely not what I would expect from Swift. For another, if that’s what the options look like, I’m probably going to conclude that the thing that I want isn’t available. I would have to go to the Internet to find out that what I wanted was v7.

Also, this isn’t just an efficiency issue. Users hate non-random randomness so much! It’s rubbing their noses in the fact that the default sort is based on a value they don’t care about and can’t even see. So while I’m waiting for UUID.timeOrdered() to come along, I’ll be default sorting by a separate property .dateAdded and hoping that the dataset doesn’t get too large. Yes, that’s a threat, and not an empty one - which is how I ran across this discussion in the first place.

This would be a very poor place to be. Most developers who want a specific type of UUID want guarantees about how it is created. It would be effectively source breaking for Swift to silently change the generating algorithm for timeOrdered UUIDs such that they are no longer adhering to the same standard.

11 Likes

Very much agree here. If I want UUIDv7, then I want UUIDv7 until I intentionally change the behavior. There are many things to which this logic generally applies (sort algorithms, etc.) but all the UUID versions have very specific tradeoffs and it is hard to pick a generically “best” one. I think picking a new underlying version for “time ordered” after the initial release would be really unfortunate.

This is why I quite like the new .version7() spelling. And the corresponding v4 function — I think it’s plenty fine to have both that and the init.

For discoverability I would prefer to add some guidance to the docs on UUID with general guidelines about which to pick when you don’t what version to use, followed by a link to the RFC.

Always choose version 7, and you'll never regret it. But learn how to use it: if necessary, use a timestamp offset, and if you need uniform distribution between shards, use 32 (or fewer) least significant random bits. In fact, the new RFC 9562 standard was created exclusively for UUIDv7 and to remove obsolete text from the old standard.

UUID.v5(namespace: "", name: "")
UUID.v8(Data())
UUID.v7()

This format is better IMHO.

When using at places where the type is already defined (such as calling a function or creating a structure where variable is defined as UUID), it also allows to use dot notation .v7() instead of .init(v7), which is nicer and fits Swift better.

7 Likes

The enum argument won me over, +1 to UUID.v4() et al.

1 Like

I do sympathize with the want to have human readable names for these versions though, especially as UUID for years chose a (sane) default of ".random()" — I don't see why we can't provide both (one aliasing to the other), and documenting the choice as unchanging into the future.

Notably, the human-readable variants are leagues more readable in post-authorship code:

MyObject(id: .random())
MyObject(id: .namespaced("MyObject"))
MyObject(id: .timeOrdered())
MyObject(id: .custom(customData))

VS

MyObject(id: .v4())
MyObject(id: .v5(namespace: "MyObject"))
MyObject(id: .v7())
MyObject(id: .v8(customData))

BTW, why having empty brackets for cases without parameters?

The names could combine both, eg.

public static var v4Random: Version { get }

public static var v7TimeOrdered: Version { get }

Do you mean UUID.v4? I imagine that might be confusing, as UUID.v4 != .v4, which is slightly less ambiguous as .v4().

What do you mean by this: UUID.v4 != .v4 ?

I see no problem if UUID.v4 was a static computed property similar to Date.now.

This actually illustrates a different point: we really shouldn't allow users to pass different types of UUIDs to the same parameter, as virtually all uses of UUID want one particular type. Really the only time I don't care is when I'm adding a UUID for an internal type to give it identity, in which case it's really overkill.

So this raises the question, should we use different types for these? UUIDv4 (where UUID is just a typealias), UUIDv5, UUIDv7, UUIDv8, etc. Why shouldn't we do this to ensure users can't use inconsistent UUID types in their code?

1 Like