How do I cast the argument type within a closure signature in Swift?
Asked Answered
D

4

9

I'm trying to write a light observer class in Swift (currently Swift 2). The idea is to use it within an Entity Component system, as a means for the components to communicate with one-another without being coupled together.

The problem I'm having is that all types of data could be communicated, a CGVector, an NSTimeInterval and so on. This means that the method being passed could have all kinds of type signatures (CGVector) -> Void, () -> Void etc.

I'd like to be able to store these varying signatures in an array, but still have some type safety. My thinking is that the type for the array would be (Any) -> Void or perhaps (Any?) -> Void, so that I can at least ensure that it contains methods. But I'm having trouble passing methods in this way: Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.

First attempt:

//: Playground - noun: a place where people can play

import Cocoa
import Foundation

enum EventName: String {
    case input, update
}

struct Binding{
    let listener: Component
    let action: (Any) -> ()
}

class EventManager {
    var events = [EventName: [Binding]]()

    func add(name: EventName, event: Binding) {
        if var eventArray = events[name] {
            eventArray.append(event)
        } else {
            events[name] = [event]
        }
    }

    func dispatch(name: EventName, argument: Any) {
        if let eventArray = events[name] {
            for element in eventArray {
                element.action(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
            eventArray = eventArray.filter(){ $0.listener.doc  != listener.doc }
        }
    }
}

// Usage test

//Components

protocol Component {
    var doc: String { get }
}

class Input: Component {
    let doc = "InputComponent"
    let eventManager: EventManager

    init(eventManager: EventManager) {
        self.eventManager = eventManager
    }

    func goRight() {
        eventManager.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
    }
}

class Movement: Component {
    let doc = "MovementComponent"

    func move(vector: CGVector) {
        print("moved \(vector)")
    }

}

class Physics: Component {
    let doc = "PhysicsComponent"

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }
}


class someClass {
    //events
    let eventManager = EventManager()

    // components
    let inputComponent: Input
    let moveComponent = Movement()

    init() {
        inputComponent = Input(eventManager: eventManager)

        let inputBinding = Binding(listener: moveComponent, action: moveComponent.move) // ! Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'
        eventManager.add(.input, event: inputBinding)

    }
}

let someInstance = someClass()
someInstance.inputComponent.goRight()

Throws the error Cannot convert value of type '(CGVector) -> ()' to expected argument type '(Any) -> ()'.

Second attempt

If I genericize the Binding struct to recognise different types of arguments I have a bit more luck. This version basically works, but the array holding the methods is now [Any] ( I'm not sure whether it's the attempt to cast Any back to the Binding struct that is causing the slightly odd error below Binary operator '!=' cannot be applied to two 'String' operands):

struct Binding<Argument>{
    let listener: Component
    let action: (Argument) -> ()
}

class EventManager {
    var events = [EventName: [Any]]()

    func add(name: EventName, event: Any) {
        if var eventArray = events[name] {
            eventArray.append(event)
        } else {
            events[name] = [event]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).action(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
           // eventArray = eventArray.filter(){ ($0 as! Binding).listener.doc  != listener.doc } //Binary operator '!=' cannot be applied to two 'String' operands
        }
    }
}

Is there a way to do this and have the array hold methods of varying type signatures, something like [(Any?) -> ()] ?

Attempt 3...

Reading around, eg here http://www.klundberg.com/blog/capturing-objects-weakly-in-instance-method-references-in-swift/ it seems that my approach above will lead to strong reference cycles, and that what I need to do is pass the static method eg Movement.move rather than moveComponent.move. So the type signature I would be storing would actually be (Component) -> (Any?) -> Void rather than (Any?) -> Void. But my question still stands, I still would like to be able to store an array of these static methods with a bit more type-safety than just [Any].

Dealer answered 28/6, 2016 at 10:29 Comment(5)
You ought to take a look at this. Might help you.Samirasamisen
Reminds me of this ObserverSet implementation which is described here: mikeash.com/pyblog/…Hangchow
The fundamental problem here is you cannot convert a (CGVector) -> () to an (Any) -> (). You're saying that a given function that takes a CGVector input can now take any input, which isn't correct at all (functions are contravariant in this regard). I suspect you'll either have to factor out the closure storage logic and let whoever calls the dispatch method provide the closure to call, much like this github project – or as you say, you'll have to store the closures as Any and then typecast them back when you need to call them.Graz
Thank you for those links, really interesting to see various implementations of this kind of observer/ notifier pattern. I particularly liked Mike Ash's approach. In it, he has the solution/ hack it seems of how to cast a parameter inside a closure signature, which seems to be to partly recurry it: f: { f($0 as T) }Dealer
@originaluser2 I suspect you'll either have to factor out the closure storage logic and let whoever calls the dispatch method provide the closure to call the problem there is that this decoupling pattern depends on the dispatcher being completely agnostic about who may or may not be receiving its dispatches.Dealer
D
2

One approach to casting the parameters of a closure, suggested in Mike Ash's blog that Casey Fleser linked to, is to "recurry"(?) it.

A genericised Binding class:

private class Binding<Argument>{
    weak var listener: AnyObject?
    let action: AnyObject -> Argument -> ()

    init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
        self.listener = listener
        self.action = action
    }

    func invoke(data: Argument) -> () {
        if let this = listener {
            action(this)(data)
        }
    }
}

And the event manager, without the recurrying:

class EventManager {

    var events = [EventName: [AnyObject]]()

    func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {           
        let binding = Binding(listener: listener, action: action) //error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'

        if var eventArray = events[name] {
            eventArray.append(binding)
        } else {
            events[name] = [binding]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).invoke(argument)
            }
        }
    }

    func remove(name: EventName, listener: Component) {
        if var eventArray = events[name] {
            eventArray = eventArray.filter(){ $0 !== listener }
        }
    }
}

This still produces the same error, of not being able to cast to AnyObject:

error: cannot convert value of type 'T -> Argument -> Void' to expected argument type 'AnyObject -> _ -> ()'.

Bu if we call the first part of the curried function, and enclose it within a new closure (I don't know if this has a name, I'm calling it "recurrying"), like this: action: { action($0 as! T) } then it all works (technique taken from Mike Ash). I guess this is a bit of a hack, in that Swift type safety is being circumvented.

I also don't really understand the error message: it's saying it can't convert T to AnyObject, but then accepts casting to T?

EDIT: updated with the complete code so far edit2: corrected how events are appended edit3: removing events now works

//: Playground - noun: a place where people can play

import Cocoa
import Foundation

enum EventName: String {
    case input, update
}

private class Binding<Argument>{
    weak var listener: AnyObject?
    let action: AnyObject -> Argument -> ()

    init(listener: AnyObject, action: AnyObject -> Argument -> ()) {
        self.listener = listener
        self.action = action
    }

    func invoke(data: Argument) -> () {
        if let this = listener {
            action(this)(data)
        }
    }

}


class EventManager {
    var events = [EventName: [AnyObject]]()

    func add<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {

        let binding = Binding(listener: listener, action: { action($0 as! T)  }) //

        if events[name]?.append(binding) == nil {
            events[name] = [binding]
        }
    }

    func dispatch<Argument>(name: EventName, argument: Argument) {
        if let eventArray = events[name] {
            for element in eventArray {
                (element as! Binding<Argument>).invoke(argument)
            }
        }
    }

    func remove<T: AnyObject, Argument>(name: EventName, listener: T, action: T -> Argument -> Void) {
        events[name]? = events[name]!.filter(){ ( $0 as! Binding<Argument>).listener !== listener }
    }
}

// Usage test

//Components

class Component {

    weak var events: EventManager?
    let doc: String
    init(doc: String){
        self.doc = doc
    }

}


class Input: Component {

    init() {
        super.init(doc: "InputComponent")
    }

    func goRight() {
        events?.dispatch(.input, argument: CGVector(dx: 10, dy: 0) )
    }

    func goUp() {
        events?.dispatch(.input, argument: CGVector(dx: 0, dy: -5) )
    }
}

class Movement: Component {
    init() {
        super.init(doc: "MovementComponent")
    }
    func move(vector: CGVector) {
        print("moved \(vector)")
    }

}


class Physics: Component {
    init() {
        super.init(doc: "PhysicsComponent")
    }

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }

    func move(vector: CGVector) {
        print("updated \(vector)")
    }

}

// Entity

class Entity {

    let events = EventManager()

}


class someClass: Entity {

    // components
    let inputComponent: Input
    let moveComponent: Movement
    let physicsComponent: Physics

    override init() {

        inputComponent = Input()
        moveComponent = Movement()
        physicsComponent = Physics()
        super.init()
        inputComponent.events = events

        events.add(.input, listener: moveComponent, action: Movement.move)
        events.add(.input, listener: physicsComponent, action: Physics.move)
    }
}

let someInstance = someClass()

someInstance.inputComponent.goRight()
//moved CGVector(dx: 10.0, dy: 0.0)
//updated CGVector(dx: 10.0, dy: 0.0)

someInstance.events.remove(.input, listener: someInstance.moveComponent, action: Movement.move)
someInstance.inputComponent.goUp()
//updated CGVector(dx: 0.0, dy: -5.0)

someInstance.events.remove(.input, listener: someInstance.physicsComponent, action: Physics.move)
someInstance.inputComponent.goRight()
// nothing
Dealer answered 28/6, 2016 at 13:14 Comment(5)
Nice solution, only thing I would mention is that your logic for adding the binding to the dictionary is slightly flawed. When you do if var you're actually creating a copy of the array in the dictionary, therefore appending to it won't affect the original dictionary. I would suggest taking advantage of the fact that optional chaining returns Void? and do the following: if events[name]?.append(binding) == nil {events[name] = [binding]}Graz
Thank you, I hadn't spotted that. I like how you use the option chain being nil as the condition for initiating the new array. I guess I hadn't spotted this because I hadn't added a second listener to the same event yet. Plus I'm still used to languages where arrays are reference types. I've updated the code above to include your change, and the second listener.Dealer
Ultimately the problem boils down to a strongly typed language really not wanting you to have an array of mixed types. Sure, you can squish them all in to [AnyObject], but then you have a nightmare of having to constantly cast everything if you want to be able to do anything with the data. So, with this "friction" in mind, perhaps the unitary "Event Manager/ message centre" model is not the one to go for and I should instead think of several "lines of communication", one for each type of data being passed around.Dealer
...Perhaps I should build the messaging service into the components themselves. Sort of like having a delegate, except you'd have an array of multiple delegates? (and it would be run-time and "hot-swappable", not compile-time like Swift's delegate protocol pattern)Dealer
Another implementation that is very similar here: blog.scottlogic.com/2015/02/05/swift-events.html Interesting to compare this with the other links that were posted above eg mikeash.com/pyblog/… and github.com/100mango/SwiftNotificationCenterDealer
D
1

A different approach to how to store a collection of different type signatures. Instead of using generics, casting, or type erasure, use an enum with associated types representing each type of signature you want to use eg

enum Signature{
    case cgvector(CGVector -> Void)
    case nstimeinterval(NSTimeInterval -> Void)

The disadvantage is that the enum captures a strong reference to the method. However (I need to take this out of a playground to test it more), this doesn't seem to create a strong reference cycle. You can set the containing entity to nil and all of its components appear to be deinitialized. I'm not quite sure what's happening there. To me this enum approach seems cleaner than putting a generic wrapper in an array of AnyObject and have to cast and type-erase constantly.

Comments and criticisms welcome.

/*:
 ## Entity - Component framework with a notification system for decoupled communications between components

 ### Limitations:
 1. Closure class stores a strong reference to the components. But, a strong reference cycle is not created. 
 2. A given class instance (component) can only subscribe to a given event with one method.
 */

import Cocoa
import Foundation

enum EventName: String {
    case onInput
}

//A type-safe wrapper that stores closures of varying signatures, and allows them to be identified by the hashValue of its owner.
class Closure {

    enum Signature {
        case cgvector(CGVector -> Void)
        case nstimeinterval(NSTimeInterval -> Void)

        func invoke(argument: Any){
            switch self {
            case let .cgvector(closure):        closure(argument as! CGVector)
            case let .nstimeinterval(closure):  closure(argument as! NSTimeInterval)
            }
        }
    }

    var method: Signature
    weak var owner: Component?

    init(owner: Component, action: Closure.Signature) {
        method = action
        self.owner = owner
    }

}

// Entity

class Entity {

    var components = Set<Component>()
    private var events = [EventName: [Closure]]()

    deinit {
        print("Entity deinit")
    }

    // MARK: component methods

    func add(component: Component){
        components.insert(component)
        component.parent = self
    }

    func remove(component: Component){
        unsubscribeFromAll(component)
        components.remove(component)
    }

    func remove<T: Component>(type: T.Type){
        guard let component = retrieve(type) else {return}
        remove(component)
    }

    func retrieve<T: Component>(type: T.Type) -> T? {
        for item in components {
            if item is T { return item as? T}
        }
        return nil
    }

    // MARK: event methods

    func subscribe(listener: Component, method: Closure.Signature, to event: EventName ){
        let closure = Closure(owner: listener, action: method)
        // if event array does not yet exist, create it with the closure.
        if events[event] == nil {
            events[event] = [closure]
            return
        }
        // check to make sure this listener has not subscribed to this event already
        if ((events[event]!.contains({ $0.owner! == listener })) == false) {
            events[event]!.append(closure)
        }
    }

    func dispatch(argument: Any, to event: EventName ) {
        events[event]?.forEach(){ $0.method.invoke(argument) }
    }

    func unsubscribe(listener: Component, from name: EventName){
        //events[name]? = events[name]!.filter(){ $0.hashValue != listener.hashValue }
        if let index = events[name]?.indexOf({ $0.owner! == listener }) {
            events[name]!.removeAtIndex(index)
        }
    }

    func unsubscribeFromAll(listener: Component){
        for (event, _) in events {
            unsubscribe(listener, from: event)
        }
    }

}


//Components

class Component: Hashable {
    weak var parent: Entity?
    var doc: String { return "Component" }
    var hashValue: Int { return unsafeAddressOf(self).hashValue }

    deinit {
        print("deinit \(doc)")
    }

}

func == <T: Component>(lhs: T, rhs: T) -> Bool {
    return lhs.hashValue == rhs.hashValue
}

//: #### Usage test

class Input: Component {
    override var doc: String { return "Input" }


    func goRight() {
        parent?.dispatch(CGVector(dx: 10, dy: 0), to: .onInput )
    }

    func goUp() {
        parent?.dispatch(CGVector(dx: 0, dy: -10), to: .onInput )
    }
}

class Movement: Component {
    override var doc: String { return "Movement" }

    func move(vector: CGVector) {
        print("moved \(vector)")
    }
}


class Physics: Component {
    override var doc: String { return "Physics" }

    func update(time: NSTimeInterval){
        print("updated at \(time)")
    }

    func move(vector: CGVector) {
        print("updated \(vector)")
    }
}

// an example factory
var entity: Entity? = Entity()
if let instance = entity {

    // a couple of ways of adding components
    var inputComponent = Input()

    instance.add(inputComponent)
    instance.add(Movement())
    instance.add(Physics())

    var m = instance.retrieve(Movement.self)
    instance.subscribe(m!, method: .cgvector(m!.move), to: .onInput)

    let p = instance.retrieve(Physics.self)!
    instance.subscribe(p, method: .cgvector(p.move), to: .onInput)
    inputComponent.goRight()
    inputComponent.goUp()
    instance.retrieve(Input.self)?.goRight()

    instance.remove(Movement.self)
    m = nil

    inputComponent.goRight()
}
entity = nil //not a strong ref cycle
Dealer answered 1/7, 2016 at 14:14 Comment(1)
enum of closures is great but to use in collection its best to wrap it in a struct that has an id you can compared with ==Ambulant
L
0

I fell into that situation but i found a cool solution with an anonymous inline function, it is like mapping here is an example


var cellConfigurator: ((UITableViewCell, _ index: IndexPath) -> Void)?

func setup<CellType: UITableViewCell>(cellConfig: ((CellType, _ index: IndexPath) -> ())?)
{
        // this mini function maps the closure
        cellConfigurator = { (cell: UITableViewCell, _ index: IndexPath) in
            if let cellConfig = cellConfig, let cell = cell as? CellType {
                cellConfig(cell, index)
            }
            else
            { print("-- error: couldn't cast cell") }
        }

}

Linnet answered 27/11, 2019 at 11:54 Comment(0)
A
0

I solved this with an enum of closures that can have different signatures, however since I needed to store them in an array I wrapped it a struct with an identifier for Equatable and if case let to search by enum, e.g.

class TableViewEvents: NSObject, UITableViewDelegate {
    
    struct Event: Equatable {

        let identifier: String
        let handler: Handler
        
        init(_ handler: Handler) {
            self.identifier = "\(type(of: self)).\(handler.description).\(UUID().uuidString)" // e.g. Event.didSelectRowAtIndexPath.6730330A-FE95-4537-87BF-30955F340F8E
            self.handler = handler
        }

        static func == (lhs: TableViewEvents.Event, rhs: TableViewEvents.Event) -> Bool {
            lhs.identifier == rhs.identifier
        }
    }
    
    enum Handler {
        case didSelectRowAtIndexPath((UITableView, IndexPath) -> ())
        case willDisplayCellForRowAtIndexPath((UITableView, UITableViewCell, IndexPath) -> ())

        var description: String {
            String(describing: self).replacingOccurrences(of: "((Function))", with: "")
        }
    }
    
    var events: [Event] = []
    
    func add(_ event: Event) -> Self {
        events.append(event)
        return self
    }
    
    func remove(_ event: Event) {
        events.removeAll { e in
            event == e
        }
    }
      
    func didSelectRowAtIndexPath(_ handler: @escaping (UITableView, IndexPath) -> ()) -> Self {
        return add(Event(.didSelectRowAtIndexPath(handler)))
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        events.forEach { event in
            if case .didSelectRowAtIndexPath(let handler) = event.handler {
                handler(tableView, indexPath)
            }
        }

Used like:

        let events = TableViewEvents()
            .didSelectRowAtIndexPath { tableView, indexPath in
                print("didSelectRowAtIndexPath")
            }
            .willDisplayCellForRowAtIndexPath { tableView, cell, indexPath in
                print("willDisplayCellForRowAtIndexPath")
            }
        
        tableView.addEvents(events)
Ambulant answered 27/1, 2024 at 14:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.