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 Message
s. 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.)
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")
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With