Support for pure setters

IMO, a good setter behaves like it's setting a variable, because this is exactly what the assignment operator suggests. That is:
you should be able to replace a sequence of writes to a setter by a single write with the last value and still have essentially the same end result;
you should be able to reorder setter operations and still essentially get the same end result.
"Essentially the same end result" means that it's okay if the logging output is different or if you wasted cycles in the process.

This.

On the other hand, we have WatchKit as a counter-example. I'm surprised nobody mentioned it. It's full of setters for properties that match the typical UIKit properties, but cannot be read.

A.

I ended up here after following Set-only subscripts and not being convinced by Dave's argument that x[b] = y implies that x[b].mutatingMethod() may be called. I've had similar difficulties in grasping that a mutating method can reassign self which I, ignoring the underlying practicalities, still consider a weird and very narrow use case.

and

But how is this still true with non-copyable types? Yes, foo can be read but it can't be copied without a consume operator, in which case it's no longer possible to reassign it since it's out of scope. Doing otherwise is essentially the same as a set-only property, no?

Noncopyable types can be read through a borrow without consuming them, and a mutation generally involves getting exclusive access to the existing value first, mutating the value during the exclusive access, and then releasing the exclusive access, still implying that there is a value that could be read to start with.

Yes and no:

struct Inner: ~Copyable {
    private(set) var val = 0
}
struct Outer: ~Copyable {
    private var storage = Inner()
    var inner: Inner { storage } // 'self' is borrowed and cannot be consumed
}

Seems like there's no way (that I know of) to expose a private ~Copyable as borrowed. private(set) var inner: Inner seemingly compiles for structs, but both completely break classes:

class Outer {
    private var storage = Inner()
    var inner: Inner { storage } // Copy of noncopyable typed value. This is a compiler bug.

    private(set) var storage2 = Inner()
}

print(Outer().storage2.val) // Copy of noncopyable typed value. This is a compiler bug.

In fact, ~Copyable properties of classes seem to be write-only:

class Outer {
    var storage = Inner()
}

print(Outer().storage.val) // Copy of noncopyable typed value. This is a compiler bug.
Outer().storage = Inner() // πŸ”₯🐢πŸ”₯ this is fine

Presumably, this is a bug as removing private(set) from Inner seemingly compiles.

I guess what I'm wondering is how many assertions regarding who can do what have to have failed for this behavior to be present?

Reducing exclusive access to mutations is something that I thoroughly respect and adhere towards, sometimes at my own peril. It is unfortunately besides the current point.

Since implying is different from asserting and considering the recent inclusion of non-copyable types (bugs notwithstanding), would you agree that your statement from 8 years ago is no longer representative of the current status of the language?

My endgame here is to be able to implement subscript(_ key: Key, default: Value) { set { ... } } instead of the crime against history that is dict["foo", default: "bar"] which is objectively worse than dict["foo"] ?? "bar"

Yeah, that's a known limitation. Getters have to produce owned values, so they can't be used to abstract over storage without consuming it. If you access storage directly, though, you can borrow it. Unofficially, you can implement inner as a _read coroutine:

struct Outer: ~Copyable {
    private var storage = Inner()
    var inner: Inner { _read { yield storage } }
}

As the error indicates, that's a bug. On a top-of-tree compiler, it works properly:

class Outer {
    private var storage = Inner()
    var inner: Inner { storage } // error: 'self.storage' is borrowed and cannot be consumed

    private(set) var storage2 = Inner()
}

print(Outer().storage2.val) // compiles successfully
class Outer2 {
    var storage = Inner()
}

print(Outer2().storage.val) // works fine
Outer2().storage = Inner() // works fine

All I was trying to say is, in order to begin that exclusive access to something, there has to be a "something" that you could have read at the beginning of the access; a mutation is still a read-write rather than a pure "write" operation. My original formulation of a mutating access still notionally applies to noncopyable types if you consider the assignments to be moves rather than copies in and out:

var tmp = foo // move initial value out of `foo`
tmp.x = value // modify it
foo = tmp // move the modified value back into `foo`
1 Like

Cool, I'm guessing that the coro semantics are optimized away, right?


Can you confirm this too:

class Outer {
    private var inner = Inner()
    func test() {
        print(inner.val) // Copy of noncopyable typed value
    }
}

The compiler sometimes has access to more information than the cpu. Isn't this one of those cases where that "something" exists in the source code but not in the binary?


Towards the broader argument, I agree that write-only types would add a lot of complexity for not a whole lot of benefit. I do however believe that there is sufficient overlap with the effort that was required by move-only types to reconsider the original arguments against this proposal (and possibly others).

That works for me with a top-of-tree compiler as well.

1 Like

The inability to have set-only variables strike me odd every time I'm writing a piece of code like this:

        init(unpacked: ExampleType) {
            var unpacked = unpacked // 😭 have to make a writeable copy for no good reason
            bytes = (0, 0, 0, 0, 0) // 😒 have to initialize it first
            writeBytes(from: &unpacked, to: &bytes, ...)
        }
        var unpacked: ExampleType {
            var bytes = bytes // 😭 have to make a writeable copy for no good reason
            var unpacked = ExampleType() // 😒 have to initialize it first
            readBytes(to: &unpacked, from: &bytes, ...)
            return unpacked
        }

where:

  • :sob: - I have to make the thing writeable even if though I am passing it to a non mutating function (via a "UnsafePointer" parameter).
  • :cry: - I have to initialise the thing first although it will be overwritten a few instructions later with a new value.

Isn’t there a withUnsafePointer function that takes the parameter without inout?

Yes, though:

  1. the actual copy still happens! (just hidden inside withUnsafePointer).
  2. it makes the code uglier than necessary. Even one extra layer is bad already, and imagine having a few source variables you are reading from:
withUnsafePointer(to: a) { ap in
    withUnsafePointer(to: b) { bp in
        withUnsafePointer(to: c) { cp in
            mergeBytes(to: &unpacked, from: ap, bp, cp)
        }
    }
}

You can make a set-only variable like this, right?

extension CGContext {
 var strokeColor: CGColor {
  @available(*, unavailable) get { fatalError() }
  set { self.setStrokeColor(newValue) }
 }
}
2 Likes

Good trick. Although it won't work in many cases as we don't have out parameters in Swift:

func writeBytes(from: UnsafeRawPointer, to: UnsafeMutableRawPointer) {}
func readBytes(to: UnsafeMutableRawPointer, from: UnsafeRawPointer) {}

struct ExampleType {
    var payload: Int = 0
    typealias Bytes = (UInt8, UInt8, UInt8, UInt8, UInt8) // will be different for differnt types
    func pack(unpacked: ExampleType) {
        var unpacked = unpacked // 😒 have to make a writeable copy for no good reason
        var setOnlyBytes: Bytes {
            @available(*, unavailable) get { fatalError() }
            set { print("write logic") }
        }
        writeBytes(from: &unpacked, to: &setOnlyBytes) //πŸ›‘ Getter for 'setOnlyBytes' is unavailable
    }
    func unpack(bytes: Bytes) {
        var bytes = bytes // 😒 have to make a writeable copy for no good reason
        var setOnlyUnpacked: ExampleType {
            @available(*, unavailable) get { fatalError() }
            set { print("write logic") }
        }
        readBytes(to: &setOnlyUnpacked, from: &bytes) //πŸ›‘ Getter for 'setOnlyUnpacked' is unavailable
    }
}

Similar situation on the setter side:

        var bytesReadOnly: Bytes {
            get { ... }
            @available(*, unavailable) 
            set { fatalError("THIS SHOULDN'T  HAPPEN!")
        }
        readBytes(to: &unpacked, from: &bytesReadOnly) //πŸ›‘ Setter for 'bytesReadOnly' is unavailable

As with getter I can remove the @available(*, unavailable) from the setter to make the fragment compilable in this case: the code will compile and run properly and "THIS SHOULDN'T HAPPEN!" will be never triggered in this example... Although without the "unavailable" you'll be able triggering it in other scenarios (like bytesReadOnly = ...) and that would crash at runtime.

Producing a value from uninitialized storage is not really "set-only" either; it's isomorphic to returning a value (and is exactly what the underlying Swift calling convention does to return a value that's variably sized or too big to pass in registers. You could use withUnsafeTemporaryAllocation, initialize the allocation, then return the pointee at the end for instance. It would be handy to have a function withUnsafeUninitializedValue(_: (UnsafeMutablePointer<T>) -> Void) -> T that put this idiom together for you, and ensured that an extra copy isn't made to return from the uninitialized storage, maybe, but that's how we would address these use cases.

3 Likes

I'm sure some folks will be aghast at the idea, but set-only stored properties would be useful to me. Sometimes I need to keep an object alive, such as if I'm referencing its interior data (happens a lot with pretty normal use of NSImage et al, for example). So I need to keep a reference to it, but solely for reference-counting purposes. I have no desire to ever [directly] access the stored value; it's just there to get the compiler-managed release call at deinit time.

By extension set-once stored properties would be valuable too, i.e. lets that cannot be read.

Maybe one day we'll have Rust-like lifetime constraints that could fulfil this sort of need, but I doubt it - I suspect there'll always be cases where that doesn't work due to limited control over 3rd party APIs, bridging to other languages, etc.

1 Like
{ _ in } (&dictionary["foo", default: "bar"])

although i find i frequently end up dispensing with the default: entirely and doing all my update logic in the closure.