[swift-evolution] [Pitch] Add the DefaultConstructible protocol to the standard library
Alexis
abeingessner at apple.com
Tue Jan 3 13:31:26 CST 2017
Since people keep chiming in with “Rust has this”, I figured I should give the context for what’s up with Default in Rust. Disclaimer: I wasn’t around for the actual design of this API, but I worked with it a lot. So any justification I give is mostly my own posthoc perception of the purpose it serves today. I’ll also be using Swift terminology/syntax here since there’s no interesting aspect of Rust involved in this design.
There are three major use-cases for Default, as I see it:
1) providing conditional default initializers for generic types
2) providing a standard hook for easily writing “obvious” default initializers
3) refining another protocol for one-off convenience methods
The first case is easy. I have a `Mutex<T>`, `Box<T>`, `Rc<T>`, etc. Generic types which require an instance of their generic type to exist. So of course their initializer requires a T. But it would be nice to not have to do this for types which have default constructors. So you have `extension Mutex: Default where T: Default`, and now you can do `Mutex()` where inference makes it clear what the type is.
Here there’s no need to care about the “semantics” of Default. We’re just saying “if you can init() I can too!”.
The second case is fairly Rust-specific, in that it combines with other features to make default initialization more ergonomic. Default provides a custom deriver, which makes a super convenient way to write default constructors for Plain Old Data types. #[derive(Default)] just says “yeah add a default initializer that loads up every field with its default”. Often this is done on a concrete type full of integers/optionals, in which case it’s synonymous with zeroing.
Since initializers in Swift are totally first-class, one could conceivably create this kind of Derive system without the need for protocols. Although #[derive(Default)] is generics-aware, so it can provide conditional conformances for generic types too.
The third case is the most complex (and niche). In effect, there are several places where you can make a slightly more ergonomic thing if you refine a protocol with “has a default initializer”. These default initializers are in no sense a requirement of the protocol, so including the initializer as a requirement of the protocol is incorrect. At the same time, no one really wants a bunch of adhoc DefaultConstructibleX protocols that are used by maybe one or two functions in the entire world.
So Default is used as a universal modifier that can be applied to any protocol to create DefaultConstructibleX without anyone having to actually define or know about it. It’s a kind of retroactive modelling. If your type has some kind of reasonable default value, you conform to Default and maybe someone uses it. A particular user of `X + Default` then infers by example what a reasonable Default would mean in this context.
Examples in Rust:
* H: BuildHasher + Default — Default applies the BuildHasher’s default seeding algorithm. For some algorithms this will go out to /dev/urandom, for others this will just set it to 0. That’s the call of the BuildHasher’s designer, and is hopefully made clear in its docs. However, there’s no reason why default constructibility is fundamental to a hashing algorithm. One could reasonably make the call that there isn’t a good default, and require it to be manually constructed. Possibly they could provide a couple wrappers which provide a clear default (MySecureHasher, MyWeakHasher).
This constraint is used by HashMap<K, V, H> ’s default constructor. So in a sense this is just a more complex version of the first case, but we’re definitely inferring some semantics here. If a Default implementation doesn’t exist, then one must use HashMap’s with_hasher constructor to provide an instance of BuildHasher.
* R: Rng + Default — same basic idea. Default seeding strategy so you don’t have to pass an instance of Rng. No reason why all Rng’s must be default constructible.
* T: Extend + Default — if something can be Extended and provides a default constructor, then presumably it’s some kind of collection. So default is presumed to be the empty collection. Again, Extend is more primitive then collections — one of the ends of a channel reasonably implements Extend, but default construction doesn’t make sense in that context. This is used by
partition<C>(predicate: (Item) -> Bool) -> (C, C)
where C: Extend + Default
which is basically just:
var yes = C()
var no = C()
for x in self {
if predicate(x) {
yes.extend(x)
} else {
no.extend(x)
}
}
return (yes, no)
This is used similarly for unzip, which converts Iterator<(A, B)> to (CollectionOfA, CollectionOfB)
This case represents a situation where the Rust and Swift devs have diverged a bit philosophically. There’s a tendency in the Rust community to make small “lego” protocols which you snap together to get the semantics you want on the off chance there’s some weird type that doesn’t fit the mold. Swift tends to try to provide more fleshed out hierarchies.
This is partially influenced by the fact that fancy hierarchies are a lot easier to write and work with in Swift. Swift has much better support for retroactive modelling and crazy super-protocol-requirement-filling extensions. Defining things like Collection correctly in Rust has also generally been regarded as “needs some kind of higher kinded types” to handle the lifetime variables on Iterators.
All in all, I don’t really have an opinion on whether Default makes sense for Swift. Haven’t thought about it all that much. Swift is still missing the features that make case 1 and 2 even viable motivations (conditional conformance and derive annotations), and I believe already mandates default initializers in several of the cases where 3 is relevant.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170103/67a5cbf8/attachment.html>
More information about the swift-evolution
mailing list