Why doesn't this do what I thought it would?


(Davidbaraff) #1
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?


(^) #2

this is gonna get real weird real fast

what’s happening here is is a combination of OpentimeErrorThrower’s value semantics and something called instance method currying. when you write

et.returnOrThrow(someIntFunction(&et.resultCode))

it’s equivalent to writing

OpentimeErrorThrower.returnOrThrow(et)(someIntFunction(&et.resultCode))

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.

  1> var x:Double = 1.5
x: Double = 1.5
  2> x.addingProduct({ x.round() ; return 0 }(), 0)
$R0: Double = 1.5 // NOT 2.0!

which is, equivalent to

  1> var x:Double = 1.5
x: Double = 1.5
  2> Double.addingProduct(x)({ x.round() ; return 0 }(), 0)
$R0: Double = 1.5

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 self inout. anything someIntFunction does to it will then be visible to the method.


#3

I'm not sure mutating should affect the behavior here.

struct HaveResult {
  var resultCode = 0

  func returnResultCodeNormal(_ value: Int) -> Int {
    return resultCode
  }
  mutating func returnResultCodeMutating(_ value: Int) -> Int {
    return resultCode
  }
}

func changeResult(r: inout HaveResult) -> Int {
  r.resultCode = 1
  return 0
}

func testNormal() -> Int {
  var r = HaveResult()
  return r.returnResultCodeNormal(changeResult(r: &r))
}
func testMutating() -> Int  {
  var r = HaveResult()
  return r.returnResultCodeMutating(changeResult(r: &r))
}

print(testNormal()) // -> 0
print(testMutating()) // -> 1

I feel like this is a bug. At least, counterintuitive.


(^) #4

no, just part of the concept of instance methods.

In c, there’s only two things to evaluate:

  1. the function you’re calling
  2. the arguments
  3. the body of the function

in that order.

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.


#5

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.


(Davidbaraff) #6

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.


(^) #7

yes, because classes are passed by reference, so an update anywhere is immediately visible to anyone with a reference to the class.

no, it will. usually most of the retains and releases will get optimized out however, if the class doesn’t “escape” the scope.