Implicit guarding self in closures

Inspired by SE-0365, with the implicit self after guarding that self exists before continuing, else returning. I wonder if a further improvement like this could be done:

{ [guarded self] in
    dismiss()
}

where guarded self takes care of the boilerplate to ensure self is around (else return) that is often used in cases like this:

{ [weak self] in
    guard let self else { return }
    dismiss()
}

Thank you for the consideration!
-BJ

3 Likes

When this has come up in the past the main objection raised has been that the most straightforward version of this feature is pretty narrowly applicable: Void-returning closures for which a simple return use the correct choice can benefit.

This isn’t necessarily a fatal flaw, but IMO a proposal in this space should either argue that this case is so common that it deserves a dedicated piece of sugar to address this pain point (as was argued for the if let x proposal) or else propose a more general design that does become widely applicable enough to carry the weight of introducing an addition piece of syntax to the language.

3 Likes

Thanks for the quick reply!

While I cannot speak for the community at large, only from my own anecdotal experience, the paradigm is used widely in closures executed after some asynchronous task, which are (...) -> Void. And I acknowledge that this may slowly become obsolete with async/await handling things differently, but...

A more general-purpose design doesn't seem fruitful -- for other cases, continue to use weak or unowned, and carry on as normal. This case would be purely additional and for the times when there is a need to exit early from a closure via guard, in which the weakly captured object is first examined and causing a return if it determines the object is no longer around. Many asynchronous closures have a signature (...) -> Void; I cannot think of one off-hand that isn't Void-returning, as usually the value obtained from the async task is passed in as a parameter to the closure, not returned. Thoughts?

Maybe this?

func foo(execute: @escaping (...) -> Int) {}
func bar(execute: @escaping (...) -> String?) {}

foo { [guard self] -> 42 in ... }
bar { [guard self] -> nil in ... }

// equivalent to:

foo { [weak self] in
    guard let self else { return 42 }
    ...
}

foo { [weak self] in
    guard let self else { return nil }
    ...
}
1 Like

The other key distinction between this and SE-0365 is that the latter does not elide control flow, which has been one of the other central objections raised when this idea has been brought up.

3 Likes

I think it would be more interesting to move the closure.self == nil check to the caller so it always looks like a strong reference at point of use. Basically it would be a weak reference in disguise as a strong reference.

There could be an attribute on the type in the function (or instance variable) that consumes it to indicate it is the callers responsibility to check for nil. Weak references are always messed up by beginners, so this would move some of that burden to the library writer. This would also give the library writer an easy way to deallocate the closure if it is no longer needed.

Maybe call the attribute @delegate.

// an optional that acts like weak except it checks if a contained `self` is nil
var callback: @delegate (Int) -> Void

func doSomething(callback: @delegate (Int) -> Void) {
    // callback can only be set in a weak variable
    self.callback = callback
    callback?() // retains enclosed `self` and calls function if `self` is not nil
    // releases `self` so it can be deallocated if this was the last reference  
}

doSomething { [self] in
    // No guard needed. This is a new type of closure that is 
    // strong while the closure is executing and weak when not executing.
    someMethodOnSelf()
}

Any escaping closure that used as a delegate could be marked this way by the library writer. I think self could be forced weak for backwards compatibility if one needs to target an earlier version of an API that added a @delegate attribute later.

1 Like

Not sure how that's possible, as:

// self can be checked and available here
doSomething {
    // and it can be gone by the time control gets to here
    someMethodOnSelf()
}

Good point. Maybe it would need to increase the reference count of self at the beginning and decrease it at the end (caller side). Basically only weak when the closure isn't executing.

That's true... however, unowned is also a valid keyword with quite obscure semantics, and is able to crash the entire program.

So in order of control flow impact, I'd say weak < guard < unowned.

There are at least three other common cases that could regularly do without an else clause. At present, we haven't had async throws long enough for people to widely recognize that a generalized Error for default catch clauses fits the pattern (it's not specific to async but the early days of Swift's standard library gave people the idea to use optionals instead, for synchronous returns). But the first two examples below are well-established.

Note: Although this topic is "guarding self", and that's the most common use case, self is not specialβ€”any weak capture could benefit.

final class Class { }
let instance = Class()
typealias SimplestCompilingExample = Void
let void: () -> SimplestCompilingExample = { [weak instance] in
  guard let instance else { return }
}
let optional: () -> SimplestCompilingExample? = { [weak instance] in
  guard let instance else { return nil }
}
let `throws`: () throws -> SimplestCompilingExample = { [weak instance] in
  guard let instance else {
    struct Error: Swift.Error { }
    throw Error()
  }
}
let result: () -> Result<SimplestCompilingExample, Error> = { [weak instance] in
  guard let instance else {
    struct Error: Swift.Error { }
    return .failure(Error())
  }
  return .success(())
}

I think every one of those guard statements should be replaceable with

[guard weak instance] in
2 Likes

Yet another possibility:

func foo(execute: @escaping (...) -> Int) {}

foo { [guard self else return 42, π‘Ÿπ‘’π‘ π‘‘ π‘œπ‘“ π‘π‘Žπ‘π‘‘π‘’π‘Ÿπ‘’ 𝑙𝑖𝑠𝑑] in
    ...
}

works with other variables as well as self.

I don't think there is a need to repeat weak after guard – what else can be guarded other than weak? unowned can't.

guard by itself is not enough for optional reference types; it would eliminate the ability to have two options:

var instance: Class?
[guard instance]
[guard weak instance]

I don't see this as a capture list-specific issue. The more general form is to be able to eliminate all:

  1. redundant bindings (the let self part from the original post)
  2. "default" else clauses

So, simplifying/clarifying things by removing reference semantics and using Bool?:

I suggest that while what's being proposed here should allow you to get rid of the same boilerplate as for the most important weak self use case, but simplifying the syntax for the second check would be a matter for another proposal, or not worth consideration.

Currently compiles:

var bool = Optional(Bool())
let closure = { [bool] in
  guard let bool, bool else { return }
}

With elided else:

let closure = { [bool] in
  guard let bool
  guard bool else { return }
}

Move the guard to the capture list, where lets are already implicit:

let closure = { [guard bool] in
  guard bool else { return }
}

When I pitched this a few years ago, the core team decided not to run a review for it and shared this feedback:

These days, especially after SE-0345 (slightly reducing the verbosity of guard let self conditions) and SE-0365 (allowing implicit self for weak self closures), I tend to think that the existing weak guard pattern is brief enough that it doesn't need to be sugared. I don't really think there's a way to further simplify this pattern without obscuring control flow.

8 Likes

While, Combine certainly has moved the mark on this, I still think that a strong capture is almost always the correct way to capture self in closures. This is true for completion handlers, animation blocks, async dispatches, alert button handlers, and other closures that are executed a finite number of times, and then released.

IMHO, it is almost always preferable to use the default strong capture. However, for event handlers, Combine operations, signal handlers and other multi invocation closures, capturing self weakly or unowned is more often the right thing to to. But even if Combine has moved the mark on this, I still think it is preferable to be explicit about both the capture semantics, as well as how to handle released captures.

In fact, blindly capturing weakly followed by a guard and early exit, very often leads to subtle bugs. All non-strong captures should force the programmer to reason about the lifetime and capture semantics of their objects. At least until a time when a no-ceremony capture can be guaranteed by the compiler to be correct, methinks.

4 Likes