Default-initable Protocol

I've come across factory-type situations where I've ended up making a protocol to represent that "this type can be constructed with a default init that takes no arguments".

I've found myself wondering (a) whether this protocol already exists somewhere in the standard library, and (b) whether Swift could auto-conform any type to it if an init() exists for it.

Is this already a thing? Would there be some negative performance implication?

1 Like

I've had similar situations.

Here are my thoughts on the solutions you mentioned:

a) Introducing a protocol in the standard library might be a sensible, pragmatic solution for everyone to consume across libraries — similarly to how Result was introduced to standardize a commonly used pattern.
b) Making the protocol auto-conforming would make it a bit too magical in my view, since AFAIK no other protocol has this behavior. And, it would also be trivial as a consumer to extend eligible types that don't conform to the protocol out of the box, if needed.

1 Like

Can you give an example of generic code that you would write against this protocol? All that you can do in an algorithm with such a constraint is initialize zero or more objects, so it’s not totally clear how you would use the protocol.

7 Likes

Perhaps have a look at this talk by Rob Napier.

It clears up some misconceptions I had about (certain) protocols and it uses.
It talks about ‘init()’ too if I remember correctly. Long story short, it’s (probably) a bad idea. Try to rethink your approach.

My favourite takeaway from those talks: “why yes, you could maybe do that. But what does it mean?”

4 Likes

An example I found in my own code recently, which prompted me to finally post this question, was an extension I wrote to NSRegularExpression.

It allowed you to pass a string, along with a dictionary which mapped key paths from a type T to regex captures. It performed the match, extracted the captures, then used the mapping to fill in and return a new instance of T.

The implementation created a new T using init(), and then uses the match information from the reg exp and the dictionary to set properties.

Looking back at the code now, I'm not quite sure why I decided to use init(), rather than making an init that took the values & key paths, and requiring T to implement that. Arguably that would have been a cleaner implementation.

There may have been a reason for preferring init(). I suspect it might be simply that it allows a struct to be defined with default values for all members, which in turn means you get an auto-generated init(), which means less work to use it in this particular case.

As @Terje suggests, perhaps it's never the best solution, but I think it's cropped up multiple times for me. I'm not saying I always get it right, but I do usually think these things through and weight up the pros & cons :slight_smile:

DefaultConstructible is a good name I think. As @davdroman says, it feels like a pattern that may recur often enough to warrant a standard name.

Sorry, when I first read your comment I failed to register that you were linking to an actual pitch.

That's good then - the discussion has taken place, and my work here is done!

I'll now go and read that thread...

I don't want to comment on the question if this protocol is needed or not, because I don't really have an opinion about that, but I want to say, that DefaultConstructible is not a good name. That is, because, AFAIK, Swift doesn't know the term 'construct'. It's called 'initialize' instead. So the protocol would have to be called DefaultInitializable to fit into Swift.

3 Likes

The danger with a DefaultConstructible protocol is that it would be abused. At first glance you might think you need init(), you see this protocol and think "aha, problem solved. I'll just conform to this". When you (probably) didn't and you should think a bit more about it.

The absence of this protocol or init() in general does exactly that. It forces you to think about it, then - like I did - grumble about it's absence and then stumble across Rob's talk. :stuck_out_tongue:

Although I am probably not expressing all of the nuances of Rob's talk, I do agree with him. I'm parroting him basically.

The absence of init() made me think more about what I am doing. Thus I think in the long run it would prevent some bugs or result in a better architecture.

Therefore for me it's a -1.

About your concrete example. Yup, I went through something similar. I recommend you use something like this:

init(from: aDictionary)

Having said all that, while in general init() does not mean anything, it could still make sense for your type. If it really does, then you should use it. Nothing is stopping you from doing so.

1 Like

I hesitate to re-open a discussion from 2016, so I'll leave the other thread alone, but I think I have a general use case which I don't see mentioned there. The discussion in that thread seems to have focussed on the idea that the standard library itself might need to use DefaultConstructible.

My case for it is different: that it supports writing generic code which uses the following pattern in its implementation:

  • make an instance of a type with default property values (that are defined elsewhere)
  • selectively mutate some (but not all) of those property values
  • return the instance

The reason why this is better than, for example, init(from: aDictionary) is that Swift already supports a nice compact way to define a struct, supply default values, and auto-generate init().

If you were required to conform to another variation of init, you'd have to find a way to supply those default values to that init. I guess could do that by calling self.init(), but you'd have to do it explicitly for every class you wanted to conform, which means making a whole load of boilerplate.

Good point!

I guess I don't feel so bad about my choice of thread title then.

How can you "selectively mutate some (but not all) of those property values" in generic code if the generic constraint you're using only guarantees that an init() exists?

The answer to this is likely "I will combine it with some other protocol to form a useful constraint", in which case my question is "why doesn't that protocol require init()?"

6 Likes

Probably the most generic abstraction that is useful alongside init() might be append(contentsOf:). As a single protocol:

protocol Appendable {
  init() {}
  mutating func append(contentsOf other: Self)
}
Those familiar with the concept may prefer the name monoid: https://en.wikipedia.org/wiki/Monoid

Swift's collection APIs could inherit from this protocol (or combination of DefaultConstructible & Appendable). This would allow for a bit more generality when writing code that can create/combine things that aren't collections.

1 Like

Using key paths constrained to the type, for example. Which can be a parameter without necessarily needing to be part of the protocol that tells you that T must implement init().

There are (at least) two questions here.

One is "why might you want to be able to require that types to conform to init?". I think I've given an example above.

Another is "why should DefaultInitable be in the standard library - why not just make a new protocol each time?". My answer to that would just be to avoid multiple re-inventions of that very simple protocol with slightly different names! Perhaps that's too low a target to justify inclusion though.

A third question (which was part of my original post) being: "given that we've got DefaultInitable, could we auto-conform things to it if they implement init()?".

I can see that philosophically the answer to that is likely to be "yes, we could, but no we won't, as it's not the Swift way".

The basic issue here is that a type can form a monoid in more than one way (and does in general), and those ways can all have different identities. I know you know this, but I want to spell it out clearly for everyone. Types are not monoids themselves, a monoid is a tuple (type, operation, identity).

E.g. the following are all monoids defined on Int¹ (written as (operation, identity)).

  • (+, 0)
  • (*, 1)
  • (min, .max)
  • (max, .min)
  • (&, -1)
  • (|, 0)
  • (^, 0)
  • ...

The spelling init() for the identity can only work for one of these monoid structures. That's OK in some cases; array concatenation and integer addition make a fair bit of sense to privilege. But it's not a very flexible model of the very general notion of "monoid" for the standard library to vend.


¹ strictly speaking, trapping overflow makes Int.+ and Int.* not associative, and hence these are not monoids. But &+ and &* do form monoids.

10 Likes

Thanks for the link to the talk @Terje - it was excellent.

He does mention DefaultConstructible (and references a quote by Dave Abrahams about it).

I'm not sure if I can quite express this correctly, but it feels to me that there are two ways of looking at DefaultConstructible.

One interpretation is perhaps a little purist: that it's a statement about the suitability/safety of using objects which have been created in their default state (and tied up with functional/mathematical notions of identity). People seem to be concerned about what the default state means, whether there's an equivalence with the concepts of zero or null, and whether the existence of this protocol will encourage clients of the library to use some types in unsafe/unexpected ways.

That's all well and good, and I can fully appreciate that the designers of the standard library are wary about adding things to it that send the wrong message to users of the library.

My interpretation is a little bit more pragmatic and laissez-faire however. I just see it as a way of being able to say "actually this type does have some rational defaults, and I'm happy for you to give them to me if you can't do better". This is far more tied in my mind to short-lived value types which probably usually exist to represent some extracted information (eg during parsing of something). :slight_smile:

1 Like

This is definitely a problem for certain types. Even languages that embrace the concept must use new types (RawRepresentable wrappers, basically) for such conformances instead.

I believe @anandabits had an alternate design that worked around the problem, though.

Swift could do the same and prefer wrapper types (or property wrappers) for such witnesses, or Swift could avoid problematic conformances entirely. At the end of the day, though, if Swift doesn't want to provide the abstraction, we're free to do so ourselves in libraries that need it :)

2 Likes

I encountered this exact scenario while working on a toy Entity-Component-System (ECS) data structure. For those unfamiliar, an ECS architecture is often used in real-time simulations such as video games. The attributes of each world object (“entity”) are stored in an “array of structs”, each element of which contains an entity identifier and all of the attributes for that entity. (This is very similar to how a relational database stores a table.)

Spawning an entity consists of appending an element to the array-of-structs. Keeping track of which fields are initialized would incur a large performance penalty, and it would spread initialization out over simulation time, which is awkward and lead to bugs. Instead, default values are needed.

My first instinct was to define a default initializer:

protocol Entity {
    var entityID: Int { get }
    init()
}

struct MyGameEntity : Entity {
    var entityID: Int
    var transform: Point3D
    var orientation: Quaternion
    var hitRadius: Double
    // etc.

    init() {
        entityID = 0
        transform = .zero
        orientation = .zero
        hitRadius = .zero
    }
}

But this “claims” the default initializer for something that is very specific to the data structure it will be stored in. I quickly replaced it with a “default” property:

protocol Entity {
    var entityID: Int { get }
    static var default: Self { get }
}

struct MyGameEntity : Entity {
    var entityID: Int = 0
    var transform: Point3D = .zero
    var orientation: Quaternion = .zero
    var hitRadius: Double = 0
    // etc.

    var default: Self {
        return .init()
    }
}

Which allows MyGameEntity to fully specify the semantics of init(). Unfortunately, this immediately turns Entity into a protocol with Self requirements, which makes it so much more difficult to work with. I wonder if the ongoing discussions about improving such types will reduce the pressure for something like DefaultInitializable.

4 Likes
Terms of Service

Privacy Policy

Cookie Policy