Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic messages in message-based architecture

I am experimenting with message-based architecture in Swift. I am trying to do something similar to the Elm Architecture, for example. This is how my code looks:

enum SideEffect<Message> {

    case sendRequest((String) -> Message)
}

protocol Component {

    associatedtype Message

    mutating func send(msg: Message) -> [SideEffect<Message>]
}

struct State: Component {

    var something: String?

    enum Message {

        case downloadSomething
        case receiveResponse(String)
    }

    mutating func send(msg: Message) -> [SideEffect<Message>] {
        switch msg {
            case .downloadSomething:
                return [.sendRequest(Message.receiveResponse)]
            case .receiveResponse(let response):
                something = response
                return []
        }
    }
}

So the state is modelled by State and you can change it by sending Messages. If there are any side effects to compute, they are returned as a SideEffect message and will be taken care of by someone else. Each SideEffect message takes a “callback” argument, a Message to send when the side effect is finished. This works great.

Now, what if I want to have a generic side effect message? I would like to have something like this:

struct Request<ReturnType> { … }

And have a related side effect to load the request and return a value of type ReturnType:

enum SideEffect<Message> {
    case sendRequest(Request<T>, (T) -> Message)
}

But this (obviously) doesn’t compile, as the case would have to be generic over T. I can’t make the whole SideEffect generic over T, since there’s other side effects that have nothing to do with T.

Can I somehow create a SideEffect message with a Request<T> that would later dispatch a Message with T? (I think I want something like this feature discussed on swift-evolution.)

like image 936
zoul Avatar asked Sep 16 '17 06:09

zoul


1 Answers

You'll want to type erase T – usually this can be done with closures, as they can reference context from the site at which they're created, without exposing that context to the outside world.

For example, with a mock Request<T> (assuming it's an async operation):

struct Request<T> {

    var mock: T

    func doRequest(_ completion: @escaping (T) -> Void) {
        // ...
        completion(mock)
    }
}

We can build a RequestSideEffect<Message> that holds a closure that takes a given (Message) -> Void callback, and then performs a request on a captured Request<T> instance, forwarding the result through a (T) -> Message, the result of which can then be passed back to the callback (thus keeping the type variable T 'contained' in the closure):

struct RequestSideEffect<Message> {

    private let _doRequest: (@escaping (Message) -> Void) -> Void

    init<T>(request: Request<T>, nextMessage: @escaping (T) -> Message) {
        self._doRequest = { callback in
            request.doRequest {
                callback(nextMessage($0))
            }
        }
    }

    func doRequest(_ completion: @escaping (Message) -> Void) {
        _doRequest(completion)
    }
}

Now your SideEffect<Message> can look like this:

enum SideEffect<Message> {
    case sendRequest(RequestSideEffect<Message>)
}

And you can implement State like this:

protocol Component {
    associatedtype Message
    mutating func send(msg: Message) -> [SideEffect<Message>]
}

struct State: Component {

    var something: String

    enum Message {
        case downloadSomething
        case receiveResponse(String)
    }

    mutating func send(msg: Message) -> [SideEffect<Message>] {
        switch msg {
        case .downloadSomething:
            let sideEffect = RequestSideEffect(
                request: Request(mock: "foo"), nextMessage: Message.receiveResponse
            )
            return [.sendRequest(sideEffect)]
        case .receiveResponse(let response):
            something = response
            return []
        }
    }
}

var s = State(something: "hello")
let sideEffects = s.send(msg: .downloadSomething)

for case .sendRequest(let sideEffect) in sideEffects {
    sideEffect.doRequest {
        _ = s.send(msg: $0) // no side effects expected
        print(s) // State(something: "foo")
    }
}
like image 104
Hamish Avatar answered Nov 02 '22 05:11

Hamish