I hit a case that I can't figure out, and for which I'm not sure there is a good answer.
If I have an effect which captures a callback, (A -> B) -> Effect<Never>, which will run continuously and repeatedly invoke the callback, then this functionality is now outside the reducer world.
This is okay if my callback is a pure function and I write lots of tests for it. However, if the computation depends on side effects, it poses a number of problems. In particular, the callback may depend on the state from the reducer, and also any effects that it does run may need to feedback into the reducer.
The only way I see to partially rectify that is to pass a scoped store into the closure such that a getState and send function can be provided. However, not even that helps the case where the effects may be asynchronous and the closure clearly needs to provide synchronous response. That leads to thoughts about promises/futures or other more traditional concurrency tools
that live firmly outside the reducer architecture.
We hit this in the case of a micro HTTP server that we need to host, but it is certainly a more general problem. For our case we were able to make the callback pure. If that were to change, we'd likely still model the HTTP service layer compositionally, but it would represent unattached state in our application.
Any thoughts are welcome. This is the demo code I put together to demonstrate the issue.
func never<A>(_ _: Never) -> A {}
struct Effect<A> { let run: (@escaping (A) -> Void) -> Void }
extension Effect {
func map<B>(_ f: @escaping (A) -> B) -> Effect<B> {
Effect<B> { cb in self.run { cb(f($0)) }}
}
}
struct Reducer<S,A> { let reduce: (inout S, A) -> [Effect<A>] }
struct Store<S,A> {
let send: (A) -> Void
init(_ initialState: S, _ reducer: Reducer<S,A>) {
var state = initialState
func send(_ action: A) {
reducer.reduce(&state, action)
.forEach { $0.run(send) }
}
self.send = send
}
}
// -----------------------------------------------------------
// Vendor API, in this case an HTTP service
struct Service {
let listen: ((String) -> (Int, String)) -> Void
}
// -----------------------------------------------------------
struct Environment {
// An opaque handle for mocking purposes
struct ServiceHandle {
// An effect to wrap the vendor listen API. This takes
// a callback that will be invoked repeatedly.
var listen: (@escaping (String) -> (Int, String)) -> Effect<Never>
}
// Start/create to get an instance of the opaque handle
var start: Effect<ServiceHandle>
// Some other effect
var readFile: (String) -> Effect<String>
}
extension Environment.ServiceHandle {
static let mock = Environment.ServiceHandle(
listen: { serviceCallback in
Effect { _ in
let request = "GET /index.html"
print("got request: \(request)")
let response = serviceCallback(request)
print("sending response: \(response)")
}
}
)
}
extension Environment {
static let mock = Environment(
start: Effect { callback in
callback(.mock)
},
readFile: { fname in Effect {
print("load: \(fname)")
$0("abkjhckjakjh") }
})
}
var Current = Environment.mock
struct ServiceState {
var service: Environment.ServiceHandle? = nil
}
enum ServiceAction {
case onLoad
case started(Environment.ServiceHandle)
}
let reducer = Reducer<ServiceState, ServiceAction> { state, action in
switch action {
case .onLoad:
return [Current.start.map(ServiceAction.started)]
case .started(let handle):
state.service = handle
// If requestHandler is pure, this is *okay*...
let requestHandler = { (request:String) -> (Int, String) in
// ...but what if we want to run an effect to inform our result?
var result: String?
Current.readFile("foo.txt").run({ result = $0 })
guard let contents = result else {
return (404, "Not found")
}
return (200, contents)
}
return [handle.listen(requestHandler).map(never)]
}
}
let store = Store(ServiceState(), reducer)
store.send(.onLoad)
I hit a case that I can't figure out, and for which I'm not sure there is a good answer.
If I have an effect which captures a callback,
(A -> B) -> Effect<Never>, which will run continuously and repeatedly invoke the callback, then this functionality is now outside the reducer world.This is okay if my callback is a pure function and I write lots of tests for it. However, if the computation depends on side effects, it poses a number of problems. In particular, the callback may depend on the state from the reducer, and also any effects that it does run may need to feedback into the reducer.
The only way I see to partially rectify that is to pass a scoped store into the closure such that a
getStateandsendfunction can be provided. However, not even that helps the case where the effects may be asynchronous and the closure clearly needs to provide synchronous response. That leads to thoughts about promises/futures or other more traditional concurrency toolsthat live firmly outside the reducer architecture.
We hit this in the case of a micro HTTP server that we need to host, but it is certainly a more general problem. For our case we were able to make the callback pure. If that were to change, we'd likely still model the HTTP service layer compositionally, but it would represent unattached state in our application.
Any thoughts are welcome. This is the demo code I put together to demonstrate the issue.