Avoiding unbreakable reference cycle with value types and closures?

It's also worth noting that all of this can only happen with @escaping closures, because (1) with a non-escaping closure you know when the closure is going to be destroyed, and (2) you can't assign a non-escaping closure anywhere, which prevents the cycle from being formed in the first place.

What I tried to say is that, despite the fact that we use let a in both the local context and closure context, it should be treated as different point of access.
It doesn't get deallocated until both scopes expire (local context finishes execution and closure is deallocated), even semantically this would be the case.

We then get the behaviour similar to reference type that multiple points of access is pointing to the same instance.
So we could use the same solution by letting programmer decide which point of access is to be treated as weak.

Agreed. Though it doesn't undermine my POV above, still, we can then limit our attention to those capturing scenarios.

I see, you want a to be inaccessible (well, nil) after exiting the scope where it was originally declared. Can I ask what that would improve?

It really wouldn't do much more than avoiding reference cycle for value type, it's more of an ad-hoc suggestion than a full-fledge idea.

The thing I originally wanted to point out is that same variable in different scope should be treated as different points of access and that some ideas can go from there.

1 Like

Cycle discussion aside, I agree that this is pretty surprising (I've been writing a lot of Swift code, for years, and I never knew about it). I'm not sure how many people expect value-types to behave in this way.

var a = [1, 2, 3]
let closure = { print(a) }
a.append(4)
closure() // prints: [1, 2, 3, 4]

Jordan's comment about capturing the 'variable' rather than the 'value' makes it seem like intended behaviour, though. Is there some strong motivating reason for that?

5 Likes

I strongly agree with the last comment. This was also a surprise to me, I always thought that value types are captured by value with a copy, not by reference. I understand it's late to change the behaviour, but I wish that a copying capture was the default.

1 Like

That's how closures work in most languages that have closures and mutable lexical bindings, going back to Scheme. There are benefits to always capturing by copy, for sure, but then you limit what can be done with closures as control flow constructs. In something like x.forEach { a.append($0) }, you would expect a to receive the mutations that occur inside the loop.

12 Likes

While I don't use this behaviour either, I think it's easier to see if we throw in multiple closures.

func foo() -> (()->(), ()->()) {
    var a = [1, 2, 3]
    let printing = { print(a) }
    let adding = { a.append(4) }
    return (printing, adding)
}

let (printing, adding) = foo()
adding()
printing()
adding()
printing()

// [1, 2, 3, 4]
// [1, 2, 3, 4, 4]
1 Like

That's a really good point. Didn't think about that :thinking:

Yeah, this would still fall afoul of the "weak references need a strong reference to keep the value alive" principle. While it may avoid a cycle, it also means you couldn't usefully pass the closure out of the variable's original scope, since the reference would immediately go to nil, unless you happened to also have another closure somewhere else keeping the variable alive too. (And if you don't need to pass the closure out of the original scope, it's probably a good idea to try to make it work as a non-escaping closure instead.)

5 Likes

Just FYI, Objective-C defaults to capturing the value:

- (void)test1 {
    int x = 1;
    void (^doit)(void) = ^void(void) {
        NSLog(@"inside block:%i", x);
    };
    x = x + 1;
    doit();
    NSLog(@"outside block:%i", x);
}

Output:

inside block:1
outside block:2

In order to be able to assign to a local variable, you need to declare it specially as "__block"

- (void)test2 {
    __block int x = 1;
    void (^doit)(void) = ^void(void) {
        x = x + 1;
        NSLog(@"inside block:%i", x);
    };
    x = x + 1;
    doit();
    NSLog(@"outside block:%i", x);
}

Output:

inside block:3
outside block:3

I don't really know what that means for circular references. Have to investigate some more...

3 Likes

Yeah, Objective-C is a bit of an outlier in regards to similar languages. AIUI that design was forced by technical limitations in gcc.

1 Like

But rather than adding these mechanisms directly to classes, it might be more interesting to explore making Swift's efficient-copy-on-write patterns easier to adopt.

Interestingly enough, I came to this thread looking around for discussion of this. I'd be interested in writing a proposal to make a @cow attribute or similar, sounds like there's precedent for this?

What? And I thought I understand how value types work...

That's why there is "capture list" ;-)

var a = [1, 2, 3]
let closure = { [a] in print(a) }
a.append(4)
closure() // prints: [1, 2, 3]
1 Like

You might like this then:

struct Reference<T> {
  var value: ()->T
  
  init(_ x: @autoclosure @escaping ()->T) {
    value = x
  }
}