struct OpentimeErrorThrower {
var resultCode = 0
var details = ""
func returnOrThrow<T>(_ value: T) throws -> T {
if resultCode == 0 {
return value
}
throw Error("oops")
}
}
func doesntWorkCorrectly() throws -> Int {
var et = OpentimeErrorThrower()
// I thought that if someIntFunction modified et.resultCode,
// then when returnOrThrow() was called, it would see that modified value of resultCode.
// Nope! In this version, returnOrThrow() sees resultCode as being zero.
return try et.returnOrThrow(someIntFunction(&et.resultCode))
}
func worksFine() throws -> Int {
var et = OpentimeErrorThrower()
let result = someIntFunction(&et.resultCode)
return try et.returnOrThrow(result)
}
So I'm trying to make a really concise error bridge. I thought I could simultaneously call the real function, someIntFunction(), which on failure sets resultCode to non-zero, and in the same line, pass back the value and have returnOrThrowError() gate on resultCode. But my first version never sees an updated value of resultCode. The second version, which unnests the call, works as expected.
Why doesn't the first version work as I thought it might?
where that first part is a static method on OpenErrorThrower, which takes an instance of OpenErrorThrower (et) and returns a function that takes your value T and does the same thing as your returnOrThrow method, except it can be called by itself instead of needing you to write self. or et. before it. the instance of self is “saved” inside the function’s context. and since it got saved (read: copied) before you called someIntFunction, the resultCode inside the function’s copy of et has the value 0 in it. it doesn’t matter what someIntFunction did to the actual et, because it won’t affect the copy, because its type has value semantics.
You can see the same behavior in the standard library types.
luckily, it’s pretty easy to fix this problem. just mark returnOrThrow as mutating. then OpentimeErrorThrower.returnOrThrow(OpenErrorThrower) -> (T) throws -> T will become OpentimeErrorThrower.returnOrThrow(inout OpenErrorThrower) -> (T) throws -> T, which captures selfinout. anything someIntFunction does to it will then be visible to the method.
in swift, it’s exactly the same. the only difference is in c, step 1 just consists of resolving a function pointer, with no context. in swift, step 1 consists of capturing the instance itself, since instance methods have context: otherwise self wouldn’t be accessible by the method’s implementation. the only question is whether self should be the same instance as the instance in the scope the function call takes place in (pass by inout), or just a copy (pass by value). really weird, but not a bug.
While we’re at it, lest not forget SE-0042 which would remove the currying part.
Though the behaviour would largely remain the same if arguments are still evaluated from left to right.
I’m not sure how it would interact when one of the arguments is inout though.
It appears that changing from a struct to a class also "fixes" the issue (i.e. makes it do what I thought it would).
So a related question is if I wrote code like
func something() {
var x = SomeClass()
}
and x doesn't leave the scope of something(), and the class is really really "simple" is it possible that I won't incur any dynamic allocation costs? i.e. the reason for using a struct is that we don't want to dynamically allocate memory and then free it up, just to handle the (unlikely) case of an error occuring. So switching to a class would be fine if I had a reasonable guarantee that in this sutation, the compiler is smart enough not to bother with all the ARC'iness of dealing with classes, given that it's pretty clear this class instance is super short lived.
Although, marking the struct version as having a mutating function isn't so bad either.