Nice way of copying an immutable value while changing only a few of its many properties?

I believe that's been discussed but hasn't been implemented, despite default values now being allowed in the synthesized init as of SE-242. There are a variety of synthesized init evolution proposals out there, so we'll see if any of them are accepted.

1 Like

I saw this in the SE:

Notice the d variable does not get an entry in the memberwise initializer because it is a constant whose value is already assigned. This behavior already exists with the current initializer.

So he doesn't think let with default should be overridable? I hope it's not the case when this SE is implemented, perhaps with some kind of annotation that this let with default is overridable.

struct Blah {
     @overridable let a = 100.   // <== something like this
}

SE-242 has already shipped with Swift 5.1. Any changes to the handling of let properties would occur in a future proposal.

Ah, I see. Is there some place/forum where I can make suggestion to get what I want?

Another thing I want is not have to care about init parameter ordering:

struct BlahBlah {
    var a: Int = 5
    var b: Int = 6
    var c: Int = 7
}

let y = BlahBlah(c: 1, a: 2)   // <== error: argument 'a' must precede argument 'c'

So the compile already know the ordering, why not just do it and not bother with the error?

The synthesized init is no different than having manually written an init which initializes properties in declaration order. And order matters in Swift, so you can't really have both.

If you want to discuss the let with default value issue, feel free to search for one of the previous discussions and see if you have anything to add.

Because parameter order matters in Swift. So the following is fine (ie no invalid redeclarations):

struct BlahBlah {
    var a: Int = 5
    var b: Int = 6

    init(a: Int, b: Int) {
        self.a = a; self.b = b; print("I'm one initializer.")
    }
    init(b: Int, a: Int) {
        self.a = a; self.b = b; print("I'm another initializer.")
    }

    func foo(x: Bool, y: Bool) { print("I'm one method.") }
    func foo(y: Bool, x: Bool) { print("I'm another method.") }
}

This will not change (as it would be a hugely breaking change).

1 Like

That is expected behaviour, because you’ve given an initial value to a constant (at the declaration site) it cannot be changed. Although there have been discussions around relaxing this restriction, see Explicit Memberwise Initializers

1 Like

For now, my solution to this is a macro that synthesizes a func that takes optional arguments for all the type's fields. This avoids making the fields mutable.

import CopyWithChanges

@CopyWithChanges
struct Report {
    let venue: String
    let sponsor: String?
    let showAttendanceByDate: [Date: [(String, Int)]]
}

The code after expansion:

struct Report {
    let venue: String
    let sponsor: String?
    let showAttendanceByDate: [Date: [(String, Int)]]

    public func with(venue: String? = nil, sponsor: String?? = nil, showAttendanceByDate: [Date: [(String, Int)]]? = nil) -> Self {
        Self (
            venue: venue ?? self.venue,
            sponsor: sponsor ?? self.sponsor,
            showAttendanceByDate: showAttendanceByDate ?? self.showAttendanceByDate
        )
    }
}

How to use it:

let r1 = Report(venue: "Gardenia", sponsor: "Exty One", showAttendanceByDate: [Date(): [("Premiere", 128)]])

// changes venue and attendance
let r2 = r1.with(venue: "Aloha", showAttendanceByDate: [:])

// change sponsor to nil
let r3 = r1.with(sponsor: .some(nil))

I apologise if some better solution appeared in the mean time. If so I'd be glad to hear it.

Edit: the macro can be found at https://github.com/entonio/CopyWithChanges

3 Likes

Have you thought about allowing r1.with(sponsor: nil) to be used to set an optional value to nil? You would have to make .some(nil) the default value for optionals. On one hand this would have the disadvantage that with(nonOptional: nil) would be a noOp and with(optional: nil) would change the value, but on the other hand .some(nil) is less discoverable and less clean on the call site. :thinking:

No, I hadn't pictured using .some(nil) as the default value for optionals at all :person_facepalming:

And I would use something like field: field == .none ? nil : self.field in the init call, was that your idea or did you have some other?

On the one hand, it annoys me that I would be treating fields differently (rather than just adding a level of optionality to all). The .some(nil) default also looks weird, but maybe it patterns better with the ??. The call site would look much better, although whoever is used to using facultativa arguments may assume nil as a noop, but then again maybe not.

I had to read this a number of times to understand it, but yes, you mean that apparently equal nil at the call site would have different behaviours. I hadn't thought of that as an objection, mostly because nonOptional: nil is not something people will be doing anyway, but the inconsistency still bothers me.

I'm not too concerned with the discoverability, but there's always the possibility that you inherit a codebase with this and there is absolutely nothing in the code that will point a newbie to .some(nil).

As to the call site, while I agree it looks better, it may also be deceptive.

What I think tilts the balance in the direction of using nil is that you don't always have a literal to pass to it, in fact fairly often you may have a variable that may well be an optional itself, and it would be very misleading or even unwieldy to require .some(variable) for optionals. So, even if using .some(nil) as the noop default for optionals feels wrong, I think I'll have to do it. Would the semver experts weigh in on whether this requires version 2.0.0 or 1.1.0? I knew I should have used 0.x for the first releases!

On the other hand it's a good thing that I published this before you raised this question, otherwise I'd just have left it blocked.

I've changed the macro according to @let_zeppelin 's suggestion.
The new version is 1.1.0. I hope nobody was yet using the original one.
The package is on github as mentioned above.

import CopyWithChanges

@CopyWithChanges
struct Report {
    let venue: String
    let sponsor: String?
    let complexStructure: [Date: [(String, Int)]]
}

The code after expansion:

struct Report {
    let venue: String
    let sponsor: String?
    let complexStructure: [Date: [(String, Int)]]

    public func with(venue: String? = nil, sponsor: String?? = .some(nil), complexStructure: [Date: [(String, Int)]]? = nil) -> Self {
        Self (
            venue: venue ?? self.venue,
            sponsor: sponsor == .none ? nil : self.sponsor,
            complexStructure: complexStructure ?? self.complexStructure
        )
    }
}

How to use it:

let r1 = Report(venue: "Gardenia", sponsor: "Exty One", complexStructure: [Date(): [("Premiere", 128)]])

// changes venue and attendance
let r2 = r1.with(venue: "Aloha", complexStructure: [:])

// changes sponsor to nil
let r3 = r1.with(sponsor: nil)
2 Likes

The current best definition for with is:

func with<T: ~Copyable, E: Error>(_ object: consuming T, update: (inout T) throws(E) -> ()) throws(E) -> T {
    try update(&object)
    return object
}
1 Like