PrePitch: Add `default initializer` protocol to the standard library

With property wrappers around the corner, I wanted to gauge interest on a simple protocol addiction to the standard library. The idea of a type being able to signal that it has a default initializer

protocol DefaultInitializable{
  init()
}

This would allow anybody to create default value behavior by using a property wrapper like @DevAndArtist shows below.

The ultimate goal would be to provide a way to specify that a type has a default value. Other languages use expressions default value expressions - produce the default value for any type - C# | Microsoft Learn but I think Swift's version of the same feature could be cleaner if we had a common protocol.

What does the community think? Better name ideas?

3 Likes

For reference: [Pitch] Add the DefaultConstructible protocol to the standard library

Honestly, I don't share your end goal at all. I'm not looking forward to using @Default instead of simply typing = "" or = [] or = [:]. I find the current way better.

But... I happen to have a similar protocol in my code for an entirely different reason: it allows me to have dictionaries where every key has a value and you don't have to deal with nil. Pretty convenient for situations where a default "empty" value is desired.

The protocol looks like this:

protocol DefaultFormRepresentable {
	static var defaultForm: Self { get }
	var isDefaultForm: Bool { get }
}

A bit more code later, this is the kind of things it enables:

var candidatesByCity: DefaultValueDictionary<String, Set<String>> = [:]
candidatesByCity["London"].insert("Mary")
candidatesByCity["London"].insert("Jeremy")
candidatesByCity["London"].remove("Mary")

var pointsByClient: DefaultValueDictionary<String, Int> = [:]
pointsByClient["Naomi"] += 10
pointsByClient["Matt"] += 12
pointsByClient["Naomi"] -= 10
var clientsWithPoints = pointsByClient.nonDefault.keys

This is painful with regular dictionaries because you have to handle nil everywhere.

2 Likes

I think for this use case something like python defaultdict would be better, because you don't always want to use the same default value. In this example you cannot use init() for damage multiplier

var damageMultiplier = DefaultDict<String, Int>(defaultValue: { 1 })
damageMultiplier["sniper_rifle"] = 3
var damageTaken = DefaultDict<String, Int>(defaultValue: { 0 })

func attack(_ person: String, with weapon: String) {
    let standardDamage = 100
    damageTaken[person] += standardDamage * damageMultiplier[weapon]
}

attack("Alice", with: "pistol")
attack("Bob", with: "machine_gun")
attack("Bob", with: "sniper_rifle")
print(damageTaken["Alice"]) // 100
print(damageTaken["Bob"]) // 400

This was actually my original design, but I ended up changing it. The protocol version avoids having to specify the default value again and again in the initializer. It also avoids storing a copy of the default value within each dictionary. And DefaultValueDictionary use the isDefaultForm property to remove values that are set to their default form, keeping the dictionary clean of default values.

In the example above, I'd be inclined to model the weapon as:

struct WeaponStats {
   var damageMultiplier = 1
   var firingSpeed = 10
   var reloadTime = 25
}

And then give that type a default form. Don't be shy of using structs.

1 Like

I'm not sure I'd want it when you can't get much shorter than = .init() needed.

I find providing default at subscription point to be more useful than at type level and variable initialization so far. Esp. when I tend to give them different defaults at different phases of the program.

The only advantage I see in this pitch is that you can make use of the protocol if it existed and write a custom property wrapper type and then compose it with other property wrappers so you get implicit initialization on all types that adopt the protocol for free. However you can retrofit that behaviour manually today, which could be a good or bad contra argument here as well.

This idea has been extensively discussed (hundreds of messages) already; @Tino has given the link to that discussion. Essentially the same arguments apply today and I would simply refer you to my replies there.

And here I was thinking this was going to be a slam dunk. :slight_smile:

On the past thread there seems to be a notion that all types need to conform to the protocol. I don’t think so but there are already types in the that provide default initialization that would make sense for them to conform.

We didn’t have property wrapper back then so I am struggling to come up with a usecase that does not envolve property wrappers.

@Default var foo1:Optional<String> // 

It is true that any package can provide conformance for this trivial functionality. In my view this is very similar to wanting to include the identity protocol from SwiftUI in the standard library.

In pure efficiency terms, writing = nil after the end of the declaration is less characters than @Default. I don't really see what the motivation is here to wrap this functionality. It's making something more implicit — for what gain?

If you have custom types and want to promote specific configurations for them, you can declare something like static var default: MyCustomType on the type and then clients can just type = .default to use the suggested value.

var foo1:MyType? // implicit nil, not what I want. 
var foo2:Optional<MyType>  // doesn’t provide implicit init, 

// I want to encapsulate the notion of default init for the ones that already provide in the standard library
var foo3:MyType? = MyType() 

// contrived end goal example which could be built
@Default var foo4:Optional<MyType>

// every other property builder could then provide a default value not just my package. 

// provides just one level of optionality
@FlatOptional var foo5:MyType?????

To (hopefully) summarize that other thread: a default-initializer protocol is a nice syntactic interface, but there isn’t an overlying semantic philosophy for such types. RangeReplaceableCollection and the Numeric protocol family separately declare it, because they separately need it, while they otherwise have nothing in common. RRC uses it for an empty collections, while the numeric types use it to set a value to zero. The RRC version is a requirement, while the numeric version is an extension which forwards to an actual requirement (AdditiveArithmetic.zero) that covers the same result!

You can actually write this with regular dictionaries today:

var candidatesByCity: Dictionary<String, Set<String>> = [:]
candidatesByCity["London", default: []].insert("Mary")
candidatesByCity["London", default: []].insert("Jeremy")
candidatesByCity["London", default: []].remove("Mary")
2 Likes

It's not exactly the same thing. In my DefaultValueDictionary, setting the default value will remove the entry from the dictionary.

Also, if you end up repeating this default: [] parameter everywhere it suggests you should perhaps write a wrapper for expressing things more clearly, which is what I've done. And making this wrapper generic required this DefaultFormRepresentable protocol.

I can understand why someone might desire a dictionary instance with an automatically removed default. I can also see why writing the default out with every function call can be tedious.

But what is the use case for creating custom types with per type dictionary value defaults, and what useful generic algorithms can be created by having all such types (yours and anyone else's) arbitrarily unified by a protocol?

It becomes quite handy when nesting dictionaries. Let's say we want to index text objects by the words they contain, and have a separate index per language. You can do something like this:

typealias FileName = String
typealias Word = String
var textIndexByLanguage: DefaultValueDictionary<
   Language,
   DefaultValueDictionary<
      Word,
      Set<TextObject>
   >
> = [:]

And the inner dictionaries don't have to each hold their own copy of the default value. You don't need to worry about some code being able to replace one of the inner dictionaries with one with a different default either. Memory consumption is reduced a bit too.

I don't know if this crosses the "generally useful" threshold, but since this is a thread about a default value protocol I though I'd share my use case.