$functionReturnValue

hello,

when using defer statement it is very tempting to have an ability to grab the function return value without introducing a special variable and tracking it manually. consider this example:

func foo1(param1: Int, param2: Int?, param3: UnsafePointer<Float>?) -> Int {
    
    defer { trace($functionReturnValue) }
    
    guard param1 > 0 else {
        return -1
    }
    guard let param2 = param2 else {
        return -2
    }
    guard let param3 = param3 else {
        return -3
    }
    return some(param1: param1, param2: param2, param3: param3)
}

in order to write this today you have to introduce a variable, and always remember to assign it before returning:

func foo2(param1: Int, param2: Int?, param3: UnsafePointer<Float>?) -> Int {
    
    var result = 0
    defer { trace(result) }
    
    guard param1 > 0 else {
        result = -1
        return result
    }
    guard let param2 = param2 else {
        result = -2
        return result
    }
    guard let param3 = param3 else {
        result = -3
        return result
    }
    result = some(param1: param1, param2: param2, param3: param3)
    return result
}

the obvious problem here is that it is very easy to forget to assign the variable:

    return -3

or mistakenly use a different variable:

    let result = -3
    return result

to be less verbose you can introduce a helper function:

func setResult<T>(_ v: inout T, _ val: T) -> T {
    v = val
    return val
}

and use it like so:

func foo3(param1: Int, param2: Int?, param3: UnsafePointer<Float>?) -> Int {

    var result = 0
    defer { trace(result) }
    ...
    guard let param3 = param3 else {
        return setResult(&result, -3)
    }
    return setResult(&result, some(param1: param1, param2: param2, param3: param3))
}

this is shorter but at the same time more awkward looking and is still easy to forget to do.

was ability to get the function return value discussed? how do you feel about adding this new feature to swift? this is a pure additive feature, hopefully not too hard to implement.

2 Likes

Wouldn't it be easier to copy-paste the type signature and do a rename?

func foo1(...) -> ... {
  let result = foo1_inner(...)
  trace(result)
  return result
}
func foo1_inner(..) -> ... { /* old implementation */ }

This avoids having to change each return statement in the function.

1 Like

you can say the same about the defer statement itself. if we didn't have it today, would we introduce it, or would we use this workaround?

to see the obvious limitation here imagine you want to trace some internal variable(s) of foo1_inner in addition to result.

I don't think it is the same thing. One of the main uses for defer is resource cleanup. Using a wrapper would need to extend the lifetime of the resource and obscure the logic of the code. Moreover, different paths along the function may need different resources to be cleaned up.

5 Likes

I think this is a worthwhile addition to the language. It’s fairly niche, but I can see situations where it would be useful. One question: how would this react to methods that throw? I actually wasn’t sure how Swift handles defer and throws today so I threw together a quick Playground:

enum SomeError: Error {
    case shouldErrorWasTrue
}

func doSomething(andThrow shouldThrow: Bool) throws {
    defer { print("Hello") }
    if shouldThrow {
        throw SomeError.shouldErrorWasTrue
    }
}

do {
    try doSomething(andThrow: true)
}
catch {
    print(error)
}

In this case, the defer statement is still executed. Obviously, if this was using $functionReturnValue, then it would be invalid. Would $functionReturnValue need to be a Result<T, Error> for a throwing function returning T?

defer isn't only used at the outermost function level. You can also use it within any nested scope:

func foo() throws {
    do {
        defer { print("Hello") }
        try bar()
    }
}

The defer block in that case runs when the do scope exits (either by finishing successfully or by throwing). In either case there is no return value at that point.

4 Likes

Can it be statically determined if the defer would execute because of a return statement?

Here's a thought: give the defer block a parameter which will be filled in with the return value (and of course its type must match the function's return type). If the function is exiting because of a throw, then any such defer is not executed. Or type it as Result<ReturnType, Error> if you really need something that executes in either case.

Can it be statically determined if the defer would execute because of a return statement?

Only in the most trivial cases, and those cases can easily be solved without using defer at all.

If the function is exiting because of a throw , then any such defer is not executed.

defer is useful specifically because it executes regardless of how the scope is exited. Any version that may be skipped seems pointless at best, and a recipe for bugs at worst.

Or type it as Result<ReturnType, Error> if you really need something that executes in either case.

This still doesn't cover the cases in which there is neither a return value nor an error.

1 Like

i believe $functionReturnValue shall work exactly like it was an explicit variable declared at the very beginning of the function and set to return value just before every return statement. that answers all raised questions:

  • if function type is "T" the $functionReturnValue is "T?" (see option-b below)
  • if function type is "T?" the $functionReturnValue is still "T?"
  • if function is "foo() throws -> T" the $functionReturnValue is still "T?"
  • if function result is not available (e.g. because function throws or in the "inner" defer contexts the $functionReturnValue value is nil
  • the "inner" defer statements might additionally prohibit $functionReturnValue usage and raise a compilation error.

(option-b would be to have $functionReturnValue non optional in non throwing functions and optional in throwing functions).

1 Like

The inner scope is a very interesting case. Consider this function:

func foo() throws -> Int {
    if /* some condition */ {
        defer { print($functionReturnValue) }

        if /*someOtherCondition */ {
            return 42
        }
        else if /*someThirdCondition */ {
            throw SomeError
        }
    }

    return 100
}

The defer statement there could execute when the function returns a value, when the function throws an error, or when the if statement ends but before the function returns.

1 Like

good example. it shows that even the inner defer will have return value in some cases, so worth having $functionReturnValue to be typed as optional in all instances.

2 Likes

I would suggest making it of type Result<R, E>, where R matches the function return type and E the error type (either Error or Never). This way you can trace thrown errors too.

Using $ as a prefix could conflict with the projected value of property wrappers. I'd use # instead.

2 Likes

Since Result is in the Swift standard library, doing this at the language level might instead necessitate a new type, something like this:

enum FunctionReturnValue<ReturnValue, Error> {
    case notYetReturned
    case returned(ReturnValue)
    case threwError(Error)
}

As an added bonus, if typed throws is added to the language, then the Error type of this enum could match the function’s.

1 Like

It seems like it would be a better match for what you're trying to do to just generally have some sort of "function decorator", probably as a custom attribute, that allowed you to automatically insert code that runs on entry/exit, and which would have (read-only, probably?) access to the parameters and return value.

9 Likes

This would also make it easier to make a defer that only runs if the function throws, but not if it returns successfully

As for why a function decorator wouldn't be good enough, you can't use one to imitate a defer partway through a function that uses variables defined earlier in the function

e.g.

class MyDylib {
	let handle: UnsafeMutableRawPointer
	let myfn: @convention(c) () -> ()

	init(path: String) throws {
		guard let opened = dlopen(path, RTLD_LAZY) else {
			throw LoadError.noDLL(path: path)
		}
		handle = opened
		defer {
			// Deinit doesn't run if the init fails, so we need separate cleanup
			if case .threwError(_) = $functionReturnValue {
				dlclose(handle)
			}
		}
		myfn = try getSymFromDll(handle, "myfn")
	}
}
1 Like

That does not seem like a pattern I would actually want to encourage. If you want a way to run a defer only on error paths, that's something we could support directly.

1 Like

Would you ever be able to access the return value while it has the first case?

No, the way I see it in my head the notYetReturned case would only happen for defer statements inside another scope in the function (e.g. an if statement) and therefore would not have a return value. If that if statement had a return, then it would be the returned case instead.

I think we could also have set-like syntax:

func foo() -> Int {
    defer(returnValue) {
        print(
            “Returned \(returnValue)”
        )
    }

    ...
}

In the above example ‘returnValue’ would be the default name of the defer-provided value (like how ‘newValue’ works in a computed variable’s ‘set’). That is, “defer(returnValue)” could be written as “defer” and used under the name ‘returnValue’ or a different name could be specified. As for the type, I think it should be just ‘Int’. The only ways of exiting a function’s scope is by returning a value (what we expect to happen in foo), by calling a ‘Never’-returning function (which we shouldn’t be concerned about) and by throwing (which doesn’t apply to foo). As for a throwing function similar to foo, I second the idea proposed: of the return value being an optional wrapping the actual return type.

As for source breakage, I think the only way that this proposal could be considered source breaking is by the implicit ‘returnValue’ of defer being used instead of a variable of the same name declared in the outer scope. Needless to say, this case would be incredibly rare.

What do you think?

1 Like

What about non-function scopes?