Can you mark a stored property as mutating?

If so, I don't remember the syntax.

The mutating keyword is just for methods, right? A mutable property is just a var instead of a let.

One gets the sense that you’re just waiting for someone to ask what it would mean for a stored property to be marked as mutating. Isn’t every var stored property on a struct inherently mutating set?

Yes. I'm talking about get.

struct 🏛️ {
  var property: Void {
    mutating get { }
  }
}

var instance = 🏛️() {
  didSet { print("🏺") }
}

instance.property // 🏺

Ah, ok. That's not "stored property" btw.

3 Likes

I’m not sure what a property of Void type is but maybe you mean a property that’s a function

struct Pillars {
    var property: () -> ()
}

func test() {
    let p = Pillars() { 
        print("🤷🏻‍♀️") 
    }
    p.property()
}

I know. That is why I asked the question. I already knew how to write a computed property with a mutating get. The verbose form someone might actually need looks like

private var _property: Property
var property: Property {
  mutating get { _property }
}

What does it mean for a stored property to have a mutating get?

mutating get does not imply that the get mutates the thing being got, but instead that getting it mutates the entity that defined the property. This makes it salient to have a mutating get on a computed property: there is code there that can mutate the container.

For a stored property, no code executes in get besides the copy constructor, which does not mutate its container. Definitionally then, all stored properties have nonmutating gets.

2 Likes
struct 🏛️ {
  lazy var print🏺Once: Void = print("🏺")
}

var structure = 🏛️()
structure.print🏺Once // "🏺"
🏛️().print🏺Once // Cannot use mutating getter on immutable value: function call returns immutable value

Yup. I'm going to make a fine distinction here, but I think this example does more to point out that lazy is a weird wart in the programming language than to undermine that argument.

Firstly, as noted in the introductory proposal for property wrappers, lazy amounts to a compiler special-case for the functionality now managed by property wrappers. That proposal explicitly calls out that lazy is isomorphic to a stored property and a computed property that wraps it, and provides an equivalent property wrapper for the feature.

So I think this sticks with my argument: lazy is a special-case that is as-if there was a stored property and a computed property. The stored property itself has a nonmutating get, but the computed one has a mutating get.

6 Likes

There's nothing in the language which restricts us to making sure mutation is locally meaningful, e.g.

struct PropertyObserverTriggerer {
  mutating func triggerThem() { }
}

Whatever other meaning we may use as convention, "let" basically just represents a subset of the functionality of "var". So I find it interesting that we're restricted from closing off get access, the way we can with set and subscripts/methods.

struct SupertypeOfLetAndVar {
  var varOnlyProperty: String {
    mutating get { _varOnlyProperty }
    set { _varOnlyProperty = newValue }
  }
  private var _varOnlyProperty = ""
}

If only we could omit @ before property wrappers on the use site. Then one day we could silently introduce "lazy" wrapper, remove "lazy" support from the language and users won't notice anything happened!

You have this special talent.. Maybe that's just me, but a dozen messages down the thread I still don't understand where you are getting at. This?

struct S {
    mutating func modify() {...}
    
    mutating var hohoho: String {
        // getter, where you can mutate
        modify()
        return "I just mutated self"
        // the stored "hohoho" values is not used.
        // alternatively "return value" where you use a spacial
        // `value` to return the stored value
        // similar to how we use `oldValue` / `newValue`
    }
    // setter for "hohoho" is normal.
    // S.hohoho = "hello"
    // S.hohoho // mutates
}
1 Like

Story of my life! :joy_cat: Certainly not just you. I'm not easy to deal with but at least I understand that, and still love you all anyway. :smiling_face_with_three_hearts:

…But regarding your example, not quite. If there's any computation about it, we can write that already. I was asking if we could avoid the computed+stored combination.

struct S {
  private let _c = C()
  var c: C {
    mutating get { _c }
  }

  final class C {
    var hohoho = "🎅"
  }
}

var s = S() {
  didSet {
    print("Obzurvabobble Object! 😵‍💫")
    print(s.c.hohoho)
  }
}
s.c.hohoho = "🦌"
1 Like

Restrictions on stored properties don’t ever close off capabilities from the enclosing type itself. You get a mutating setter and a non-mutating getter because that’s what’s necessary for a var; if you want to customize that, you do have to separate the “field” from the “property” and use two declarations.

“Wait, doesn’t let work like a property with no setter at all?” Kinda! In my head the additional promise of “it won’t change for the lifetime of the object” makes it more than just a property with no setter, but of course that does fall out from being a stored property with no setter. (Though I can’t remember if public let in a frozen class, an unsupported feature, also becomes an optimization promise today…) But even then, you could say let and var as stored properties both expose the minimum requirements they need to get the job done; after all, a maximally conservative getter now isn’t just mutating but throws as well.

2 Likes

Good point. :face_with_diagonal_mouth:

struct S {
  var property: Void {
    mutating get throws { try _property.get() }
  }
  private lazy var _property = Result { try Self.property }
  private static var property: Void {
    get throws { }
  }
}