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.
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 yield
ed 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.
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 .
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.
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
.