How to supply a closure to a class's init which accesses that class's instance variables?

In Swift 4, I have something like:


class Worker {   
    var iVar = 0
    var operation: () -> Void

    init(operation: @escaping () -> Void) {
        self.operation = operation
    }

    func performOperation() {
        self.operation()
    }
}


and I want to do this:


let newWorker = Worker {
    iVar += 1
}

newWorker.performOperation()


How can I make it work? I am aware of the long way, making the closure an optional variable and supplying it after the instance has been initialized, but I want to be able to do it consicely, at the point of initialization.


I want to tell the instance, "run this code in your own scope", without having to hardcode "newWorker.iVar" inside the supplied closure if possible.


Edit: clarificiation

Accepted Reply

I don't see that there's a "long way", since supplying the closure after initialization is still going to be a closure in the wrong context.

I think the “long way” is something like this:

let newWorker = Worker()
newWorker.operation = {  
    newWorker.iVar += 1  
}

which doesn’t look too bad to me.

The “short way” is a serious chicken and egg problem:

  • newWorker
    must be fully initialised before anyone outside its initialer can access
    iVar
    .
  • Part of that initialisation is setting up

    newWorker.operation
    .
  • Thus the closure being supplied to the

    operation
    parameter can’t possibly access
    newWorker
    .

One way to escaped this bind is to supply the

Worker
value to the operation as a parameter:
class Worker {
    var iVar = 0
    var operation: (Worker) -> Void

    init(operation: @escaping (Worker) -> Void) {
        self.operation = operation
    }

    func performOperation() {
        self.operation(self)
    }
}

let newWorker = Worker { op in
    op.iVar += 1
}
newWorker.performOperation()

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

It's not clear that "make it work" is a reasonable goal here. Closure syntax means what it means, and your closure is constructed in the scope of the calling type, not the called type. I don't see that there's a "long way", since supplying the closure after initialization is still going to be a closure in the wrong context.


For example, if there is a variable called "iVar" in the calling scope, what syntax tells the compiler that it isn't what the closure is supposed to capture?


Is there something neater you can do with an extension to the Worker class that adds a convenience initializer?

I don't see that there's a "long way", since supplying the closure after initialization is still going to be a closure in the wrong context.

I think the “long way” is something like this:

let newWorker = Worker()
newWorker.operation = {  
    newWorker.iVar += 1  
}

which doesn’t look too bad to me.

The “short way” is a serious chicken and egg problem:

  • newWorker
    must be fully initialised before anyone outside its initialer can access
    iVar
    .
  • Part of that initialisation is setting up

    newWorker.operation
    .
  • Thus the closure being supplied to the

    operation
    parameter can’t possibly access
    newWorker
    .

One way to escaped this bind is to supply the

Worker
value to the operation as a parameter:
class Worker {
    var iVar = 0
    var operation: (Worker) -> Void

    init(operation: @escaping (Worker) -> Void) {
        self.operation = operation
    }

    func performOperation() {
        self.operation(self)
    }
}

let newWorker = Worker { op in
    op.iVar += 1
}
newWorker.performOperation()

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

> One way to escape this bind is to supply the Worker value to the operation as a parameter


Thank you, Ser Eskimo! It's not exactly the level of terseness I was hoping for but it works well at the calling site and that's more than good enough!


I should explain what I am trying to achieve with this pattern, so that Apple may identify the use case and provide better support for it:


I am making a game using Swift 4, SpriteKit and GameplayKit. The "Worker" example above is actually a subclass of GKComponent, that I call a "CustomClosureComponent".


When I create a GKEntity, I add a CustomClosureComponent to it, and tell it which actions it should perform upon its private SKSpriteNode every time the component's update(deltaTime:) method is called per frame.


So my code was supposed to look somewhat like this:


let monster = GameEntity(name: "monster", components:
    [VisualComponent(position: point),
     AnimationComponent(frames: idleFrames),
     CustomClosureComponent {
        monstersPrivateSprite.doSomethingEveryFrame(like: this)
        }
    ])


Now I guess I'll just have to make a minor addition:


let monster = GameEntity(name: "monster", components:
    [VisualComponent(position: point),
     AnimationComponent(frames: idleFrames),
     CustomClosureComponent { component in // or leave it out and use $0, but this is clearer.
        component.publicSprite.doSomethingEveryFrame(like: this)
        }
    ])


And it works! Thanks!


As for improved syntax, do you think Swift could use a "local" or "calledScope" keyword, that refers to the scope of the called method, unlike "self" which means the scope of the caller? So that we could write:


let newWorker = Worker {
   local.iVar += 1
}

And it works! Thanks!

Yay!

As for improved syntax, do you think Swift could use a "local" keyword, that refers to the scope of the called method, unlike "self" which means the scope of the caller?

I’m not the right person to talk to about changes to the Swift language. That sort of thing is done over on the swift-evolution mailing list.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"