[Draft] Throwing Properties and Subscripts

Has there been any development regarding this topic?

How use of Optional type or Result is not sufficient enough ?

The same way as using Optional or Result everywhere instead of throws is not sufficient enough: The former loses the information about the reason why the desired outcome wasn't achieved and the latter is A: cumbersome to deal with and B: in case of a settable property allows one to set the value to an error, which is nonsensical in cases where the value itself is never meant to contain an error.

3 Likes

The code at the caller site would still had to handle failure , either it throwing or optional result. I doubt that desired behaviour can be transparent.

I think we still have a semantic problem of throwing setter used in tandem with throwing mutator (it even lurks into Modify Accessors).

I don’t see how you find this:

func doSomethingDangerous() throws {
    try callThrowingMethod()
}

Behaviorally different from this:

func doSomethingDangerous() throws {
    try accessThrowingProperty
}

Do you mean the defer pitfall? In my opinion, this is a general problem with coroutines that has less to do with property accessors and more to do with the way coroutines are (or, more precisely, aren’t) supposed to interact with errors. If we had throwing modify accessor, its apparent throws-ness would simply no longer be dependent upon the yielded block, no? Or, maybe I misunderstood you. Could you, please, elaborate?

Even with get-set

var foo: Foo {
  get { ... }
  set throws { ... }
}

try foo.throwSomething()

as is, even if throwSomething throws, the set is invoked.
If set also throws, we're now left with 2 distinct exceptions, which is quite a problem.

We may say, if throwSomething throws, then set is not called, but that is vastly different from other accessors (especially with stored variable). We may add new DoubleException: Error but it gets clunky really easily.

There's also a question if set should be called in the first place, but to say no would be source breaking. So one should weigh that option rather carefully.

This Draft PR from @suyashsrijan is on the right track!!

There’s still more work to do :-) I think the two remaining things to do is restore LValueAccessKind and support _read/_modify.

1 Like

I might miss something from the modify thread but why is it a problem?

Assume we had typed throws and both set and the method would throw different error types such as A and B. Then either we will get a variadic generic Unit type which itself conforms to Error when all its generic type parameters also conform to Error, or we fallback to the plain Error instead if there is no Unit yet.

do {
  try foo.mutateAndThrow()
} catch is Unit<A, B> {
  ...
}

do {
  try foo.mutateAndThrow()
} catch is Error {
  switch error {
  case let e as A:
    ...
  case let e as B:
    ...
  default:
    ...
  }
}

With the mentioned features we can let the compiler infer the error type automatically and box it if necessary. For now it should be fine to fallback to Error no?

Wait, why would it be source breaking when the behavior is only present in throwing setters, which are purely additive?

Given a non-throwing settable property, what happens if a mutating method throws? Is the setter called? If so, what is the newValue equal to?

Nothing is wrong really. Though I'd feel weird if the Unit is introduced solely for this usage.

And if we're to change the set semantic we might as well do that. If I interpret @John_McCall's post here correctly, that may not be off the table. Though after re-reading it, I could be totally off the mark there :thinking:.

I mean that we can either introduce set throws with different semantic and be inconsistent with set, or retroactively change set semantic which is source breaking (or do something else).

The setter is called with the value being whatever it is right before the throw happens.

Same, therefore I hope if we eventually get that type (not clear yet), it will solve more problems than this one.

1 Like

Now I see where the dual-error problem comes from. The compounding error problem exists outside this use case, though. I've found myself repeatedly hitting this design problem:

init(from decoder: Decoder) throws {
    let container = decoder.singleValueContainer()
    if let firstTry = try? container.decode(First.self) {
        // ...
    } else if let secondTry = try? container.decode(Second.self) {
        // ...
    } else {
        // What exactly do I throw here?
    }
}

On one hand, I would want to throw an aggregate error that contains the errors from each attempt, so that diagnosing the issue becomes easier. On the other hand, I'd be exposing a whole lot of implementation details by doing that and my gut tells me to define my own Error type and throw that.

Regardless of what I'd choose, the key reason why this situation is not a fundamental problem is that I do indeed have a choice and the compiler forces me to choose.

What if the compiler would also force me to choose in this exact manner in case I declared a throwing setter? The first thing that comes to mind is promoting the newValue to a Result if the setter throws:

struct A {
    enum Error: Swift.Error {
        case somethingWentWrong
    }

    var name: String

    mutating func setNameAndThrow(to name: String) throws {
        self.name = name
        throw Error.somethingWentWrong
    }
}

struct B {
    var a: A {
        get { A(name: "John Appleseed") }

        set throws { 
            // `newValue` has the type `Result<A, Error>`
            switch newValue {
                case .success(let a):
                    // Do the actual setting or throw something custom
                case .failure(let error):
                    // Throw the error directly or otherwise make the same choice as above.
            }
        }
}

var b: B()
try b.a.setNameAndThrow(to: "Bob Dylan")

Meanwhile, in the parallel universe where function parameters can also be marked as throwing, the newValue is a throwing parameter instead of a Result.