Cannot use mutating member on immutable value: function call returns immutable value: why cannot call mutating func directly on return value?

struct Foo {
    struct Bar {
        mutating func baz() { }
    }

    func getBar() -> Bar {
        return Bar()
    }
}

let foo = Foo()
// why this doesn't work?
foo.getBar().baz()  // Cannot use mutating member on immutable value: function call returns immutable value
// must use a `var`?
var bar = foo.getBar()
bar.baz()

so must assign to a var first, but the end result seems to be the same as directly calling mutating func on return value?

Try to think of it this way:

foo.getBar() returns a Bar value. If you intend on mutating this value, you need to store it somewhere. Otherwise, what's the point of mutating it? If you don't store it, then you have no way of observing its new state after the mutation.

2 Likes

What you say make sense if the mutated value is used afterward. But for an JSONEncoder:

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .custom { date, encoder in
    var container = encoder.singleValueContainer()
    try container.encode(date.formatted(.iso8601)) // what happen here?
    // nothing more
    // the code read like none sense, illogical  :( 
}

My guess is inside container it's "wired" so calling .encode(...) cause the change up in the encoder. So why would a "caller" need to use an intermediate var? Seems like unnecessary leaking of implementation details.

There are no technical reasons a compiler couldn't add the "variable storage" for you.

AFAICT, it's more about pragmatism; If you're not using the mutated values, you're likely missing something. Most commonly of these are when you use mutating function variants instead of the non-mutating ones.

Note that a function returns a value, not a variable. You can use the value, but you can't mutate a value, instead you mutate a variable.

As @Lantua said, there is probably no technical reason why the compiler can't create a temporary variable for you, but that's a bad idea (I believe) because a mutable variable should be declared explicitly.

I think the problem is the JSONEncoder's api: can the container be a let and its .encode(...) work as non-mutating? Like in SwiftUI @State you can mutating inside a value type View? This way then there is no need for an useless intermediate var.

SwiftUI @State has that behavior because it has special design in it - it saves the state value in heap, so the struct itself isn't mutated. The vanilla struct in the language doesn't have this feature.

Technically, you’re correct that encoder need not be mutating; they can offload the data onto some external allocations. You may have also realized that that applies to essentially any mutating protocol requirements.

I’d probably have fun dissecting why that leads to some very clunky implementations, esp. for Encoder. Though I don’t think that is relevant to the current discussion on the broader stroke of mutating a returned value, so I’ll refrain from doing so.

1 Like

I have often wished it was possible to call a mutating method on a function return value:

let x = foo().mutate()

Instead of:

let x: ResultType = {
  var result = foo()
  result.mutate()
  return result
}()
3 Likes

Hence the existence of functions like :

func with<T>(_ initial: T, update: (inout T) throws -> ()) rethrows -> T {
    var value = initial
    try update(&value)
    return value
}
2 Likes

Nice. That would allow method cascading at least in a minimal viable form. The thing I always wanted. Otherwise to do cascading / chaining I have to manually implement:

extension Foo {
    func mutated() -> Foo {
        Foo(x: x + 1)
    }
}

FWIW C++ allows calling mutating methods on temp values in this case.

Not sure about the benefit of this with<t>(...), @Nevin 's example is clearer than using the function.:

//let x =
with(Foo()) { foo in
    var bar = foo.getBar()
    bar.baz()
}

Not simpler and less understandable..

Q: why the return value can be discarded? Should the compiler complain with<T>(...) return is not used? Seems global func return can be ignored. No need for @discardableresult for global func?

Edit: never mind, seem in Xcode playground, no warning for func return unused. In regular project, does warn.

Your mutated() func is constructing a new copy, potentially expensive. Calling a mutating func directly is modifying the same var, quite different. Seem like do your mutated() way is not good practice?

I would have written that:

let bar = with(Foo().getBar()) { $0.baz }

But I also would probably have made getBar a computed property named bar.

2 Likes

The use case is for creating a Foo, then go through a bunch of mutating func's to configure it and in the end get a fully configured Foo, so the initial value needs to be a Foo.

In that case, doesn't bar have to be a property? Otherwise I don't see how mutating it can effect the final Foo.

That’s how JSONEncoder works and it’s my complaint that you get a “container” from it and must assign to an intermediary var in order to call a mutating func on this container. I wish you can call this mutating func directly on the func return. Calling mutating func on the container change the encoder’s encode result.

See my post above