[Pitch] - Set-only properties

This has been pitched before, but it was many years ago; I think we should allow properties to only have a setter and no getter. This is already possible by doing this:

var foo: Bar {
  @available(*, unavailable)
  get { fatalError() }
  set { ... }
}

but this syntax is non-optimal and somewhat confusing. This pattern should be supported better by swift so you can easily write a property that is "set-only".

4 Likes

A hypothetical set-only property would come with some severe limitations — it would have to be the last component of an lvalue expression, and you couldn’t pass it as an inout parameter. In fact for a set-only property to be of any use, the setter would need to have some kind of observable side effect. Why not just define a function?

4 Likes

Which would you prefer?

button.setColor(to: .red)

// or

button.color = .red

IMO, the bottom one looks better.

1 Like

I'd prefer the former, if it allows chaining:

button.color(c).font(f)...

On the need to have set-only properties - it is a "+0.5" from me. I remember I was reaching out for yours "get { fatalError() }" sometimes (never thought of using "@available(*, unavailable)" - a clever trick!).

In the following example:

func foo(x: Int) {
    var y: Int
    memmove(&y, &x, MemoryLayout.sizeofValue(x))
}

I have to (1) initialise "y" (even though it won't be read). I also have to (2) change "x" to be a variable (even though it won't be written):

func foo(x: Int) {
    var y: Int = 42 // 1
    var x = x // 2
    memmove(&y, &x, MemoryLayout.sizeofValue(x))
}

Must admit that the first thing bothers me less than the second.

Cross-linking a prior discussion on this topic: Set-only computed properties

You make a good point that out-only parameters would model something like memmove() more accurately, and inside the body of the callee, an out parameter is like a set-only property. In turn, at the call site, you would be able to pass in an uninitialized stored property or a even set-only computed property as an argument to an out parameter.

One interesting thing is that this would also allow designated initializers to share code, but it would still look funny:

struct S {
  var x: Int
  var y: Int
  init() {
    helperFunction(&self.x, &self.y)
  }
}

However it's not clear that this is any better than

(self.x, self.y) = helperFunction()

The semantics of out would also be more subtle than inout; the compiler would have to check that every control flow path through the function initializes every out parameter exactly once (if we leave it conditionally uninitialized the caller has no way of knowing the state of its uninitialized storage). This could be relaxed to "assigns at least once", because we can allow re-assignment inside the callee with a bit of work, by introducing hidden state, so for example

func callee(x: out [Int]) {
  if something {
    x = [1, 2, 3]
  }
  // more code...
  x = [3, 2, 1]
}

becomes

func callee(x: out [Int]) {
  var _xLive: Bool = false
  if something {
    x = [1, 2, 3]
    _ xLive = true
  }
  // more code...
  if _xLive {
    _destroy(&x)
  }
  x = [3, 2, 1]
}

But in any case we would need to design a new SIL analysis pass. I think the frontend also assumes in places that every property has a getter, but maybe that's no longer baked in now that we have move-only types and _read. All of this makes it a hard sell at this point, but maybe one day a really compelling use case will come along.

2 Likes

In your reading of past pitches (of which there are at least two), what arguments already made are (in your view) the strongest for and against, and in weighing those arguments what ultimately leads you to conclude that this feature “should” be supported?

Those things have clear and important use cases that contribute to the look and feel of Swift code.

That’s what is missing here: a compelling reason why property syntax must be used for this. The one real-world use case demonstrates a color property on a button, but gives no reason why it is important that this property be only writable not readable. The API if encountered in the real world would be very strange (unlike get-only properties, since these are analogous to read-only variables).

The function version makes it clear that this is one way, so seems the superior solution.

Adding complexity to the language, as set-only properties would, needs stronger motivation.

9 Likes

While "looks better" is mostly a subjective, aesthetic judgement, there's a point to be made about which form more clearly conveys the semantics of the action that's executed under the hood.

For example, for a regular get-set property, I would always, in any case, without exceptions, expect the following

var foo = Foo()

foo.bar = 42

assert(foo.bar == 42)

that is, if I set a value, I expect the same value to be retrieved in the same local scope (like, the very next line).

If more code is present between the set and get lines, my expectation wouldn't be valid, unless I'm sure that the code:

  • doesn't interact with the .bar property at all;
  • doesn't require await.

Personally, I derive from this behavior that a line like foo.bar = 42 will not produce strong side effects (save from stuff like logging, or setting other properties that derive their value form .bar).

But I see a set-only property as something that inherently produces strong side-effects (what's the point, otherwise?), for example sending the set information to a server with a background HTTP request. And in that case, as a reader of the code, I would prefer seeing a function call, instead of an assignment, because a function call communicates that there could be more going on than simple assignment.

Thus, in general I would say that I agree that

button.color = .red

looks better than

button.setColor(to: .red)

but in this particular case, I think the function form "looks better" because it conveys better the fact that something more is going on while calling the function, and this could be reflected better in the function name. For example, in the case of the background HTTP request, I would instead use a name like:

func updateBackendColor(to: Color)

Documentation could be used to clarify the semantics in both cases, but I think it's always better to use more descriptive names first, and then, if the name is not enough, add documentation.

I'm not opposed in principle to the set-only property idea, but I think it needs very good examples with sufficient generality to justify its addition.

2 Likes

Because they allow for user-defined lvalues (assignable and inout-able locations).

1 Like

Interesting fact. The following app doesn't compile:

struct T {
    var value = 0
}

struct S {
    var x = T()
    subscript(value: Int) -> T {
        @available(*, unavailable)
        get { fatalError() }
        _modify {
            yield &x
        }
    }
}

var s = S()
s[1].value = 0 // 🛑 Getter for 'subscript(_:)' is unavailable

Yet, if I comment out the "@available(*, unavailable)" notation – the app works just fine without triggering the "fatalError()" !

This is a known class of bug. Availability checking must do its own approximate analysis of what lvalue components are accessed and how, because SILGen computes the actual lowering. For example if you had foo.bar.baz = 123, then various combinations of foo, foo.bar and foo.bar.baz's getters and setters can be called, depending on mutating/nonmutating and other things. This can manifest as bogus errors when the getter and setter have different availability, among other things. A better design is to have a request to pre-compute the sequence of accesses once and then have both availability checking and SILGen work from the common description, but we haven't got around to doing this yet.

2 Likes