[safe self] in

One of the most annoying parts of Swift is the [weak self] in / guard let strongSelf = self else { return } pattern.

Example:

doSomething() { [weak self] in
  guard let strongSelf = self else { 
    return 
  }

  strongSelf.doSomethingElse()
}

Having to write all this boilerplate code takes up time and adds noise to my code.

What I want is for the compiler to handle all this for me.

Why can't we enhance Swift in such a way that:

doSomething() { [weak self] in
  guard let strongSelf = self else { 
    return 
  }

  strongSelf.doSomethingElse()
}

becomes something like:

doSomething() { [safe self] in
  doSomethingElse()
}

I propose that [safe self] in would automate the guard let strongSelf = self else { return } pattern and remove the requirement of having to explicitly use self? or strongSelf.

Taking a crawl, walk, run approach, perhaps to start, [weak self] in would only be useful when the closure return type is Void?

If it's not too controversial, perhaps it can also be useful for closures returning an optional?

This would make me so happy!

8 Likes

Hi @softwarenerd, this has been brought up on the forums a few times in the past. The reason it hasn't ever really gone anywhere is it's unclear how the feature should work for a non-Void returning closure. You may want to take a look at some of the past discussions (I may have missed a few):

That's not to say this isn't worth pursuing, but I think a new proposal would need to address some of the issues raised by past discussions.

15 Likes

The immediate thought I have is that it could just be allowed in closures that are Void returning (immediately returning if strong reference cannot be created) or in Optional returning closures (returning nil for failure to create reference). Is there an issue with that?

2 Likes

Having an extra capturing option available that only works for Void or Optional return types doesn't sounds particularly compelling to be honest. For Void, however, I can see the value and don't really see any downsides. Defaulting to nilforOptional` seems like it's not what I'd always want to do. For example, I have a section of code that resembles this:

makeNetworkCall { [weak self] (result: SomeResult) -> SomeProperty? in 
  // result.property is optional
  guard let strSelf = self else { return result.property }

  let updatedResult = self.cache.updateWith(result)
  return updatedResult.property
}

Being able to use [safe self] here, and it automatically returning nil could be an interesting source of bugs for this example.

8 Likes

This pitch doesn’t remove weak self so in situations where you don’t want to return nil you could still use this existing paradigm, or you could just nil-coalesce the return value.

2 Likes

I understand that, it’s the implicit nil return behavior itself that would be problematic IMO, it’s not always directly obvious that nil is not the default you want, and this bug could be hidden for a while in your codebase before you realize that in this specific case you needed weak self instead of safe self, not because you don’t want the automatic unwrap, but because you don’t want the implicit nil return.

1 Like

That makes sense. I’m not well-versed in Compiler Magic. Could a warning be produced for a situation like this?

Sounds like what's desired is something like:

doSomething() { [self or return 0] in
  return self.doSomethingElse()
}

As essentially just shorthand for:

doSomething() { [weak self] in
  guard let strongSelf = self else { 
    return 0
  }

  return strongSelf.doSomethingElse()
}

I think it's a nice convenience on the face of it, and there are reasonable if partially arbitrary rules for how multiple such or return … modifiers can be handled, for closures taking multiple weak arguments. Though maybe a syntax which expresses the singular return value in the case that any weak argument is unavailable would be a better solution, to avoid complexity & ambiguity. (unless the return value is a differentiating error indicator of some kind, where it might be useful to distinguish between which weak reference caused the closure to 'abort'…)

1 Like

What about doSomething() { [conditional self] in doSomethingElse() } else {return} and the else clause can be inferred as a bare return if it is Void closure, but required otherwise?

I'm not sure, not much of a compiler guru myself either

I think this makes a lot of sense for void returning closures, and agree that should only be allowed for them. That alone would make a lot of code cleaner.

2 Likes

If this is occurring often in your code, I think you're probably doing something wrong (?)

Use the implicit (strong) self for completion handlers, animation blocks, network request callbacks, alert button tap handlers, background queue dispatch, and almost everything else where the closure is guaranteed to be called exactly once (or where the closure is guaranteed to be released, such as for alert button handlers).

The only real reason you'd need the weak-strong dance is for event handlers, notification observers, signal handlers, and other such cases where the closure is registered in some storage, and then called any number of times, until explicitly de-registered.

And when you really need it, it is probably a good idea a reason about retain cycles and be explicit about your choices. Just implicitly returning is probably a bad idea, even for void-closures.

4 Likes

I'm not a huge fan of stuffing more logic into the closure signature. I mean what would be the limits of that? Could you return an arbitrary expression, or only a literal value? If it's the latter then it seems like you'd end up with many cases where you'd still need the guard statement when you can't just return a simple value.

It seems to me that guard is already the right shaped tool for the job, and maybe we just need a better guard. For instance, I think most of the problem with the current state of things is having to alias self (especially when copy/pasting a code block into a closure), and I think it would solve most of those problems to be able to do something like this:

doSomething() { [weak self] in
    guard self else { return }
    self.foo() // self is now guaranteed to be non-null
}
3 Likes

I agree, but the reception from many in these forums to any such improvements has been chilly to date.

I don't understand the details, but supposedly anything involving inferring the state of a variable (even just whether it's provably non-nil at a certain point in the control flow) is very problematic from an implementation perspective. There's also been assertions made to the effect that Kotlin does this and it turns out to not be a pleasant experience for coders, either.

In this case it's going a little further, since it's not merely a non-nil check but also a rebinding of the variable w.r.t. reference strength (presumably meaning a literal retain somewhere under the covers). I'm fine with it, but I can understand if people are a little leery about that level of 'magic'.

Aside, it's still not as readable as I'd like (no options so far are). No worse than the existing more verbose form, but it still reads weird - on face value you've already stated you're using self, which was already available in the parent context, and yet you're checking if self… what? It's learnable but not all that intuitive or transparent.

Another readability problem of the self or return 0 style is that it's even less transparent about when that conditional is evaluated. It's again quite learnable, but a beginner could easily be forgiven for thinking it's evaluated at or near doSomething call time, since while it's literally within the closure's curly braces, it's still within the declarative / parameterisation part of the closure declaration, which is conceptually evaluated more at the declaration site, IMO.

As of Swift 5, we have this:

doSomething() { [weak self] in
    guard let self = self else { return }
    self.foo() // self is now guaranteed to be non-null
}

Which is close to what you think is good. On the downside, it's marginally more verbose. On the upside, it uses well-known syntax (guard let) so it ought to be clear what it's doing.

From a larger perspective, we seem to be spending a lot of time these days on very small changes that (predictably?) don't gain much unanimous support. I'm wondering if — as a community — we should try to put an informal moratorium on fiddling with small syntax changes, and try to get people focused on some of the very big things that we're actually desperate for.

To me, the #1 big thing is an async programming model. Variadic generics are way up there too.

Besides those mountains, syntax tweaks seem less than molehills.

13 Likes

Agreed; this topic keeps coming up but fails to gain traction because (IMO) it's trying to wedge syntax where it doesn't fit and is glossing over a lot of important details.

Folks have already mentioned the inconsistencies that arise depending on the return type of the closure, and how trying to answer those questions leads to a really big jump in complexity:

  • Do we only support Void?
  • What about Optional return values, returning implicit nil?
  • Could we do something with zero?
  • Could we extend the syntax even further to let users write the default return value?

And if you try to solve all of those, then you can very well end up with something that isn't really saving you any more characters than guard let self = self else { return <whatever> }, but has increased the complexity of the compiler by a good bit and made the language harder to understand.

Folks also seem to be laser-focused on the self case and don't consider that capture lists don't just have to have self in them, but can be just about any variable that's accessible in that scope, and it's a list so there can be multiple captured variables. I haven't seen a lot of discussion about what happens in that case:

  • Should a safe self/guard self feature be restricted to self? If you think so, why not allow it for other weak captures?
  • If not, do you limit it to only a single "safe" capture per capture list? If so, why?
  • If not (i.e., you allow multiple "safe" captures), how do you describe the exit condition? Is it if they are all nil? If any are nil? What if you want one in one situation and the other in a different situation?

Breaking it down like this makes me think that this feature can only end up one of two ways, which is why it hasn't taken off: it's either so narrowly specialized that it doesn't hold its weight (in terms of implementation complexity) in the language, or it's so generalized that it becomes no more convenient than just writing the guard statements in the body of the closure.

15 Likes

I think this could be approached from a different direction. Make blocks first class citizens in the Swift type system, then extend them using utility functions.

I built something similar myself and I'm sure with more time (and the help of smarter people than I) it could be made to work in a way that more closely resembles "good" Swift syntax. More or less as follows (doing this off the top of my head):

enum Block {
    public static func weakToStrong<T: class, V, R>(_ object: T, defaultValue: R, block: (T, V) -> R) -> (V) -> R {
        return { [weak object] (value: V) in
            guard let object = object else {
                return defaultValue
            }

            return block(object, value)
        }
    }
}

If we let blocks be proper types in Swift we could just extend blocks with certain characteristics easily into dealing with these common patters (the above is nice but the syntax when using it is a bit annoying).

There’s so many of these pitches it could have its own category.

I feel like these concerns are not actually too hard to address. A simple and narrow version of the feature is sufficient and worthwile imo. I think it's fine to only support this syntax for void-returning closures. Speaking only from my own experience, but that's the only use case that I would even want to use the feature for.

A quick search of two codebases I work on finds 43 total occurrences of using guard to manually unwrap a weakly-captured self in a void-returning closure, so it's a fairly common use case for me at least. Not to mention that I would also prefer to use this syntax in many other cases where I can currently get away with optional chaning on self, and I would really end up using it quite often. Especially if capturing self this way also allowed you to omit the self prefix inside the closure, which I think would be a great convenience.

Regarding your last point, I don't see an issue with allowing multiple guard/safe captures and exiting the closure if any of them are nil. The purpose of the feature is to have non-optional references to the things you capture this way -- it doesn't make sense to try to support executing the closure if any of the captures are nil. I see this syntax as a simple translation of this:

{ [guard self, guard x, guard y] in
	// ...
}

to this:

{ [weak self, weak x, weak y] in
	guard let self = self, let x = x, let y = y else { return }
	// ...
}

All that said:

I totally agree that bigger features like those are way more valuable, so if these smaller refinements are taking significant time away from progress on those areas I would agree that it's worth holding off on them. I don't know if that's actually true or not though.

4 Likes

I don't think that syntactic sugar is the way to go here. I will argue that there is much more beautiful functional way to do it with ability to add any customisation:

func weak<T: AnyObject, U>(
  _ obj: T,
  in f: @escaping (T) -> (U) -> Void
) -> (U) -> Void {
  { [weak obj] u in
    guard let obj = obj else { return }
    f(obj)(u)
  }
}

And the usage is pretty straightforward:

class Foo {
  func handler(data: Int) { print("Data: \(data)") }

  func perform(data: Int) {
    // strong capture
    performAsync(with: data, completion: self.handler(data:))
    // weak capture
    performAsync(with: data, completion: weak(self, in: Foo.handler))
  }
}

What is nice about this, is that you can add whatever customisation points you want. You want unowned? Cool!

func unowned<T: AnyObject, U>(
  _ obj: T,
  in f: @escaping (T) -> (U) -> Void
) -> (U) -> Void {
  { [unowned obj] u in
    f(obj)(u)
  }
}
// Usage
performAsync(with: data, completion: unowned(self, in: Foo.handler))

You want default if self is dead? That is also possible:

func weak<T: AnyObject, U, Z>(
  _ obj: T,
  default: Z,
  in f: @escaping (T) -> (U) -> Z
) -> (U) -> Z {
  { [weak obj] u in
    guard let obj = obj else { return default }
    return f(obj)(u)
  }
}
// Usage
performAsync(with: data, completion: weak(self, default: -1, in: Foo.handler))

It is also possible if you want to write inplace closure instead of weird (self) -> (arg) -> T thing:

func weak<T: AnyObject, U, Z>(
  _ obj: T,
  in f: @escaping (T, U) -> Z
) -> (U) -> Z {
  { [weak obj] u in
    guard let obj = obj else { return default }
    return f(obj, u)
  }
}
// Usage
performAsync(with: data, completion: weak(self) { $0.handle($1) })

Unfortunately, in last case you are loosing nice trailing closure sugar.

It would be nice if we had variadic generics so we could write it once for any number of arguments, but for real use cases it is very rare to have callbacks with more than 2 or 3 arguments, so you need some boilerplate, but not too much.

The beauty of this solution is that it is fully in your control, there is no special compiler magic behind. It would be great, if we, as a community, will address things like generalisation features more than another special-case-sugar addition.

6 Likes