$functionReturnValue

as we saw in the examples above - non-function scoped defers can still have return values (when there was a return statement from the inner scope) and equally well function scoped defers can have no return values (when throw is used), so the two issues are somewhat orthogonal and can be resolved by either an optional value for $functionReturnType / defer parameter or the mentioned tri-state enum.

there is no breaking source change here:

defer (returnValue) { ... }

as nobody did this before (so nothing to break) - provided the old syntax is still accepted. i am ambivalent whether to use a parameter or a $functionReturnType / #functionReturnType special variables.

the thing that brought by John_McCall is interesting (a function decorator) but at the same time that approach could lead to a situation where "trivial cases are easy, hard cases impossible". i'd lean towards a more manual approach (e.g. imagine every "trace" call of the original example has a different logic in it, so if that was achieved by some property wrapper there would be a need for either different wrappers for each function or some switch/etc inside a wrapper).

while we are on this, the idea of having a language construct that allows enumeration of function parameters / return values at runtime is interesting. something more advanced than argc/argv of course and that works with arbitrary function prototypes.

defer isn't tied to only top-level scopes in functions, and it's specifically intended to be a way to nicely run clean-up code rather than being arbitrary code insertion, so the idea of parameterizing it with the return value of the function is a little bit odd. It also doesn't match tremendously well with error-handling; we'd have to tie Result into the language semantics.

The use cases you want for this feature seem to break down into two basic groups:

  • logging/tracing
  • making the defer only have effect if an error is thrown

Both of these feel like they're better addressed in more targeted ways.

The error-handling design always imagined that we'd have a way to run a defer only on errors; it's a common need, and it's easy to do, we just never picked a syntax for it. The logic would be the same for defers in all scopes, not just the outermost scope in a function.

Logging/tracing is a great match for decorators because it's a lot of boilerplate that you want to do uniformly to a large number of functions; forcing programmers to write out the logging/tracing code manually in every function really undermines that and generally leads to worse results. Like, why write this:

func scale(value: Int) -> Int {
  log.log("IntAdjuster.scale(value:): entering with value=\(value)")
  defer { log.log("IntAdjuster.scale(value:): exiting with return value \($returnValue)") }
  return value * 3
}

when you could write:

@Logged
func scale(value: Int) -> Int {
  value * 2
}

and have that default to something similar? Getting the return value is really just a fairly small part of the awkwardness here.

You can definitely imagine things that these features wouldn't work for. That doesn't always mean that hacking in something with weird semantics but that feels more general is the right language approach.

14 Likes

so long as this magic handles real life cases i am happy.

private func addressString(_ address: UnsafePointer<CMIOObjectPropertyAddress>?) -> String {
    guard let p = address?.pointee else { return "nil" }
    let selector = NSFileTypeForHFSTypeCode(p.mSelector) ?? "*** " + String(p.mSelector) + " ***"
    let scope = NSFileTypeForHFSTypeCode(p.mScope) ?? "*** " + String(p.mScope) + " ***"
    return "[\(selector), \(scope), \(p.mElement)]"
}

private func dataString(_ p: UnsafeRawPointer?, _ size: UInt32) -> String {
    guard let p = p else { return "nil" }
    let r = p.assumingMemoryBound(to: UInt8.self)
    var s = ""
    for i in 0 ..< Int(size) {
        s += String(format: "%02x ", r[i])
    }
    return s
}

private func sizeString(_ p: UnsafePointer<UInt32>?) -> String {
    guard let p = p else { return "nil" }
    return String(p.pointee)
}

private func errorString(_ err: Int32) -> String {
    if err == noErr {
        return "noErr"
    }
    return NSFileTypeForHFSTypeCode(OSType(err))
}

func ObjectGetPropertyData(plugin: CMIOHardwarePlugInRef?, objectID: CMIOObjectID, address: UnsafePointer<CMIOObjectPropertyAddress>?, qualifiedDataSize qds: UInt32, qualifiedData qd: UnsafeRawPointer?, dataSize size: UInt32, dataUsed used: UnsafeMutablePointer<UInt32>?, data: UnsafeMutableRawPointer?) -> Int32 {
    
    var err = noErr
    
    print("enter ObjectGetPropertyData(objectID: \(objectID), address: \(addressString(address)), qualifiedDataSize: \(qds), qualifiedData: \(dataString(qd, qds)), dataSize: \(size), dataUsed: \(sizeString(used)), data: \(dataString(data, size)))")
    defer {
        print("return \(errorString(err)) from ObjectGetPropertyData(objectID: \(objectID), address: \(addressString(address)), qualifiedDataSize: \(qds), qualifiedData: \(dataString(qd, qds)), dataSize: \(size), dataUsed: \(sizeString(used)), data: \(dataString(data, size)))")
    }
    // ...
}

//    enter ObjectGetPropertyData(objectID: 26, address: ['nfr#', 'glob', 0],
//      qualifiedDataSize: 0, qualifiedData: nil, dataSize: 16, dataUsed: 0,
//      data: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 )
//
//    return noErr from ObjectGetPropertyData(objectID: 26, address:
//      ['nfr#', 'glob', 0], qualifiedDataSize: 0, qualifiedData: nil,
//      dataSize: 16, dataUsed: 8, data:  00 00 39 40 b8
//      1e 85 eb )

This whole conversation is very confusing to me. Where is the functionReturnValue that $functionReturnValue is binding? Is it implicit like newValue in set blocks?

1 Like

-1

The added value doesn't feel like it outweighs the downside of another "magic" hidden variable with a name someone thought was obvious but everyone else has to try and remember.

The comment about it being "fairly niche" feels accurate. This means that code using it will be less readable and understandable to the majority who are outside this small niche when they have to read code produced by the few in the niche who use this.

Explicit code is easier for others to understand and maintain after the original author leaves the project.


Here's a possible alternative to the first alternative presented by the OP that feels less awkward and doesn't have the same issues that one does:

@discardableResult
private func result<T>(_ result: T) -> T {
  print("result is: \(result)") // or whatever your trace does
  return result
}

then can change the sample use case to something that seems clear, doesn't require defining a result variable, and doesn't allow for forgetting to assign that variable:

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

Though a more explicit name like:

private func tracedResult<T>(_ result: T) -> T {…}

(or something similar) would be more appropriate given my claim that explicit code is easier for others to maintain :)


One could also define multiple different tracers this way. Such as:

@discardableResult
func tracedResult<T>(_ result: T,) -> T {
  print("TRACE> result is: \(result)") // or whatever
  return result
}

and:

@discardableResult
func tracedResult<T>(_ result: T,
					 _ message: @autoclosure () -> String = "",
					 _ file: StaticString = #file,
					 _ line: UInt = #line,
					 _ function: StaticString = #function)
-> T {
  print("TRACE> \(message())")
  print("TRACE> returning from \(function) in \(file)\nTRACE> at line: \(line) with result: \(result)")
  return result
}

and the example function could become:

func foo3(param1: Int, param2: Int?, param3: UnsafePointer<Float>?) -> Int {
	guard param1 > 0 else {
		return tracedResult(-1, "Error: param1 needs to be greater than zero")
	}
	guard let param2 = param2 else {
		return tracedResult(-2, "Error: param2 needs to be non-nil")
	}
	guard let param3 = param3 else {
		return tracedResult(-3, "Error: param3 needs to be non-nil")
	}
	return tracedResult(some(param1, param2, param3))
}

If I call it with bad param 3:

foo3(param1: 10, param2: 12, param3: nil)

get output of:

TRACE> Error: param3 needs to be non-nil
TRACE> returning from foo3(param1:param2:param3:) in MyPlayground.playground
TRACE> at line: 1961 with result: -3

This also lets you just return a value without tracing in a way that is easily readable as such for unusual cases where you maybe called tracedResult manually or did some other manual logging.


Anyway, just an alternative idea to one work-around presented by OP that seems to address most of the issues raised without requiring new language functionality. Likely others have better ones.

(Note: if the example was a throwing function than you could define a similar function that was passed the error to throw and returned it so could throw via: throw tracedError(Errors.someError) to have a traced throw).

2 Likes