If You’re Reading .then() It’s Too Late · If we’re doing async work, effects are (almost)...
Transcript of If You’re Reading .then() It’s Too Late · If we’re doing async work, effects are (almost)...
If You’re Reading .then() It’s Too Late
Luke Westby
HandlingeffectsinJavaScript
Effect:Anyinteractionwiththe
outsideworld
const request = (url) => { return fetch(url).then((r) => r.json()); };
const getUser = (id) => { return db("users").where({ id }).first() .then((user) => user.id); };
const currentOffset = (el) => { return window.scrollY - el.scrollTop; };
Somethingsreallyshouldbetheresponsibilityofthelanguage
Callbacks
doSomeWork((error, result) => { if (error) respond(error, 500); else respond(result, 200); });
doSomeWork((error, firstResult) => { if (error) return respond(error, 500); else { processFirstResult(firstResult, (secondError, secondResult) => { if (secondError) return respond(secondError, 500); else { if (secondResult.prop) { doFirstBranchWork(secondResult, (firstBranchError, firstBranchResult) => { if (firstBranchError) respond(firstBranchError, 500); else respond(firstBranchResult, 200); }); } else { doSecondBranchWork(secondResult, (secondBranchError, secondBranchResult) => { if (secondBranchError) respond(secondBranchError, 500); else respond(secondBranchError); }); } } }); });
PromisesdoSomeWork() .then((result) => processFirstResult(result)) .then((result) => { return result.prop ? doFirstBranchWork(result) : doSecondBranchWork(result); }) .then((result) => respond(result, 200)) .catch((error) => respond(error, 500));
const myPromise = new Promise((resolve, reject) => { doSomeWorkCallbackStyle((error, result) => { if (error) reject(error); else resolve(result); }); });
}executor
Ifwe’redoingasyncwork,effectsare(almost)alwaysinvolved
Promisesrepresentthecreationofasyncwork
IfaPromiseispresent,itwecanprettysafelyassumethepresenceofaside-effect
side-effects
(user) => user.idThisissupereasytotest:
db(“users").first() .then((user) => user.id)
Thisislesseasytotest:
https://www.youtube.com/watch?v=6EdXaWfoslc
const getUser = (id) => { return db("users").where({ id }).first(); };
vs.
const getUser = (id) => ({ action: "db", table: "users", operations: [ ["where", { id }], ["first"] ] });
const dbRunner = ({ table, operations }) => { const query = db(table); return operations.reduce(/* ... */, query); };
Somewhereelse…
Declareeffectsinapplicationcode
Pauseexecutionoftheapplicationcodetoexecutetheeffect
Resumeapplicationcodewhentheeffecthasproducedaresult
Reacttype ReactDOMElement = { type: string, props: { children: ReactNodeList, className: string, ... }, key: string | boolean | number | null, ref: string | null };
Cycle.js
http://jsbin.com/numuladaqe/1/edit?js,output
Inparticular,lines10-13
const actionHandlers = { [Actions.begin](request) { return Actions.dbCall({ table: “users", operations: [ ["where", { id: request.params.id }], ["first"], ] }); }, [Actions.dbCallSuccess](request, user) { return Actions.respond(user.id, 200); }, [Actions.dbCallFailure](request, error) { return Actions.respond(error, 500); } };
server.route({ method: “GET", path: “/users/{id}", handler(request, reply) { runActions(runners, actionHandlers, request) .then((result) => { reply(result.data).statusCode(result.status); }) .catch((error) => { reply(error.message).statusCode(error.status); }); } });
const actionHandlers = { [Actions.begin](request) { return Actions.dbCall({ table: “users", operations: [ ["where", { id: request.params.id }], ["first"], ] }); }, [Actions.dbCallSuccess](request, user) { return Actions.respond(user.id, 200); }, [Actions.dbCallFailure](request, error) { return Actions.respond(error, 500); } };
const actionHandlers = { [0](request) { return dbCall({ table: “users", operations: [ ["where", { id: request.params.id }], ["first"], ] }); }, [1](request, user) { return respond(user.id, 200); }, error(request, error) { return respond(error, 500); } };
server.route({ method: “GET", path: “/users/{id}", handler(request, reply) { runActions(runners, handler(request)) .then((result) => { reply(result.data).statusCode(result.status); }) .catch((error) => { reply(error.message).statusCode(error.status); }); } });
const handler = function* (request) { try { const user = yield { type: “db", table: “users", operations: [/* ... */] };
return respond(user.id, 200); } catch (error) { respond(error.message, 500); } };
Generators
const iterator = handler({ params: { id: 1 } });
let next = iterator.next(); assertDeepEqual(next.value, { type: "db", /* ... */ });
next = iterator.next({ id: 1, name: "Luke", /* ... */ }); assertDeepEqual(next.value, respond(1, 200));
// or
next = iterator.throw(Error("NOPE!"); assertDeepEqual(next.value, respond("Luke", 500));
Twoproblems:
1.Nestedcallsareuncomfortable2.Parallelizationisn’tagiven
const handler = function* (request) { try { const user = yield { type: “db", table: “users", operations: [/* ... */] };
return respond(user.id, 200); } catch (error) { respond(error.message, 500); } };
const handler = function* (request) { try { const userId = ?? getUserId(request); ?? } catch (error) { respond(error.message, 500); } };
const handler = function* (request) { try { const userId = yield spawn(getUserId, request); } catch (error) { respond(error.message, 500); } };
OnlyonelevelofcontinuationispossiblewithGenerators
Whatifwecouldyielddeeply?
https://esdiscuss.org/topic/one-shot-delimited-continuations-with-effect-handlers
One-shotdelimitedcontinuationswitheffecthandlersSebastianMarkbåge
const funcWithEffect = (request) => { const user = perform { type: “db”, /* ... */ }; return user.id; }
try { const result = funcWithEffect(request); } catch effect -> [{ type, ...details }, continuation] { runners[type](details)
.then((result) => continuation(result)) .catch((error) => { throw error; });
}
Ifthiscanbeimplementedefficiently,itshouldbeimplemented
Again,somethingsreallyshouldbetheresponsibilityofthelanguage
@luke_dot_js@[email protected]
Questions?