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

Reply via email to