[Draft] Throwing Properties and Subscripts

Is there any news on whether or not this will ever be implemented?

Throwing properties and subscripts would we a really neat addition.

This would also allow adding dynamic throwing properties when using it along dynamic member lookup from SE-0195 which would be nice for Python interop.

4 Likes

I would also like to see this. It might also apply to both get and set if they are both supplied:

subscript(idx: Int) throws -> Element {
    get {...} //Get can throw
    set {...} //Set can throw
}

How about, if you want throws for computed properties, you just have to spell it the long way:

var x:Int {
    get throws { ... }
} 

The only other reasonable alternative IMO is:

var x:Int throws

Somehow I missed this thread when it originally happened.

One piece of concrete feedback:

I’m not sure what you’re imagining by “optimizations” here, but it’s not actually reasonable to try to prevent errors from being thrown between the start and end of an access. For example, the function taking the inout parameter might itself just be a throwing function. This creates a semantic problem if we’re required to call the setter because we might end up with multiple errors in flight at once.

I think the best solution is to revise the semantic model for all accesses to distinguish between aborting and completing an access, with the idea that aborting a mutation to storage defined with set / willSet / didSet causes the accessor call to be skipped.

2 Likes

I like this as a shorthand for declaring both the getter and setter as throwing, as well as a means for declaring a shorthand read-only computed property as throwing.

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.