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.
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution