Closed ceremcem closed 4 years ago
Backcalls aren't quite as powerful as promises. async
/await
integrates with control flow structures in a way that backcalls don't and probably never will. Consider an await
inside a loop, or inside a try
/catch
/finally
expression, and think about the hoops you'd need to jump through to translate that into equivalent code that uses callbacks. LS's backcalls are intentionally very simple; the transformations applied by JS implementations to async
/await
can get considerably more complicated, and why duplicate that work in LS?
Consider an await inside a loop
That's absolutely a necessity. I'm working around this by the following <~ :lo(op) ~>
approach:
sleep = (ms, f) -> set-timeout f, ms
debug-log = -> console.log ...[(new Date! .toISOString!), ...arguments]
i = 3
debug-log "start"
<~ sleep 1000ms
<~ :lo(op) ~>
debug-log "hi #{i}"
i := i - 1
if i is 0
return op! # break
<~ sleep 1000ms
lo(op)
<~ sleep 500ms
debug-log "end of loop."
# Outputs:
# 2019-09-17T02:25:56.952Z start
# 2019-09-17T02:25:57.953Z hi 3
# 2019-09-17T02:25:58.954Z hi 2
# 2019-09-17T02:25:59.955Z hi 1
# 2019-09-17T02:26:00.457Z end of loop.
or inside a try/catch/finally expression
Well, I've no proposal for that as I never needed such thing. I'll think about it.
It turns out I re-invented Promise
s before I get to know them, with a slightly different approach: https://github.com/ceremcem/signal. That's why I never needed Promise
s hence the async/await
.
Anyway I think I got the idea. If we have a chance to use a "default tool" for a job, you say it would probably make the job done simpler and simpler is better.
One more headache is if / else
statements with async calls:
if doc?
my-doc.attachments.push doc
else
# fetch the document `doc` and then make the attachment
err, res <~ db.get 'doc'
my-doc.attachments.push res
err, res <~ db2.put my-doc
console.log "my-doc has been saved"
Obviously above code doesn't work as it won't wait for db.get 'doc'
phase.
My workaround was using my SignalBranch
:
branch = new SignalBranch
if doc?
my-doc.attachments.push doc
else
# fetch the document `doc` and then make the attachment
my-signal = branch.add!
err, res <~ db.get 'doc'
my-doc.attachments.push res
my-signal.go!
<~ branch.joined
err, res <~ db2.put my-doc
console.log "my-doc has been saved"
Of course, it would be simpler to write it like so (I'm not sure about the syntax, I'm not familiar with await/async
):
unless doc?
# fetch the document `doc` and then make the attachment
doc = await db.get 'doc'
my-doc.attachments.push doc
err, res <~ db2.put my-doc
console.log "my-doc has been saved"
The arguments for async
/await
syntax go even deeper than simplicity. Consider your last pair of code snippets:
branch = new SignalBranch
if doc?
my-doc.attachments.push doc
else
# fetch the document `doc` and then make the attachment
my-signal = branch.add!
err, res <~ db.get 'doc'
my-doc.attachments.push res
my-signal.go!
<~ branch.joined
err, res <~ db2.put my-doc
console.log "my-doc has been saved"
I realize this is a simplified example for purpose of discussion, but nevertheless: this code has a problem. If the db.get 'doc'
call ‘returns’ an err
, this code doesn't report it and happily goes on to push my-doc
into db2
anyway. Now, of course, it's pretty easy to go in and add error-handling code... but the point is, when using await
:
unless doc?
# fetch the document `doc` and then make the attachment
doc = await db.get 'doc'
my-doc.attachments.push doc
await db2.put my-doc
console.log "my-doc has been saved"
you don't have to. In this version, an error in db.get
(or, for that matter, in db2.put
) will prevent the subsequent statements from running—just like with a synchronous call—and be captured in the enclosing Promise
, hopefully to be handled by either a regular try
/catch
or by a .then on-success, on-error
or .catch on-error
call directly on the Promise
.
So not only does await
mean writing less code, it also means handling errors for each asynchronous call isn't one more thing you have to remember to do correctly. Making fewer traps for programmers to fall into is a pretty strong reason to prefer await
, even if it didn't also make your code simpler.
For being a reference to the people like me (and to myself), here is the rewrite of above "workarounds":
sleep = (ms) ->
new Promise -> setTimeout it, ms
db =
get: (id) ->>
await sleep 2000ms
if id is 'opps'
throw new Error "There is something happened unexpected"
else if id.length > 5
throw new Error "document #id not found."
return {id, value: "the value of #id"}
put: (doc) ->>
await sleep 2000ms
if id is 'opps'
throw new Error "There is something happened unexpected"
else if id.length > 5
throw new Error "document #doc.id can not be saved."
return {doc.id, attachment-count: doc.attachments.length}
do ->>
try
my-doc = {id: "my", attachments: []}
unless doc?
console.log "Fetching the document"
doc = await db.get 'opps'
my-doc.attachments.push doc
console.log "Saving my-doc: ", my-doc
await db.put my-doc
console.log "my-doc put status:", err, res
catch
console.error "Something went wrong: ", e
do ->>
console.log "hello"
for to 5 when .. < 4
console.log "there #{..}"
await sleep 1000ms
console.log "done."
Here I guess it doesn't matter, however in general it's probably better not to use await
in loops, since you won't get batching at all.
@vendethiel Sorry, I couldn't understand. What do you mean by "getting batching"?
Sorry. I mean you get sequential and not parallel requests.
pages = []
for from 1 to 5
pages.push await get-page ..
# queries page 1, waits for the request to be complete, queries page 2, waits, ...
# VS
pages <- Promise.all [1 to 5]map get-page
# starts the 5 queries at once
Depends on what you want...
Oh, yes, I got it now. This sequential pattern is useful while implementing "reconnect/retry" pattern:
for from 1 to 3
try
ok = await serial.send-and-ack "my data"
break
unless ok
throw new Error "We couldn't send the data"
@ceremcem
i've rewritten your example with this:
# shared data
data =
error: false
doc: null
# async controller
branch = new SignalBranch {
step1: !->
if data.doc
myDoc.attachments.push data.doc
@step2!
else
err, res <~ db.get 'doc'
if not err
myDoc.attachments.push res
@step2!
else
@error!
step2: !->
err, res <~ db2.put myDoc
if not err
console.log("my-doc has been saved");
else
@error!
error: !->
data.error = true
console.log 'error occured'
}
so it's longer, but more readable, what you think? 8)
some links about this topic (in general):
@determin1st
so it's longer, but more readable, what you think? 8)
Sadly no, I don't find this more readable, because sometimes I use lots and lots of nested SignalBranch
. Something like this:
b1 = new SignalBranch
b1s1 = b1.add!
b1s2 = b1.add!
my-physical-button.on 'pressed', (button-num) ->
b1s2.go err=null, button-num
if something
err, res <~ foo
b1s1.go err, res
else
b2 = new SignalBranch
if mydoc
doc = mydoc
else
b2s1 = b2.add!
err, res <~ bar
b2s1.go err, res
err, signals <~ b2.joined
b1s1.go err, myresult
err, signals <~ b1.joined
# result will be delayed till the button is pressed
Such a logic would be hard to read for me if it's configured like in your suggestion. Moreover, I generally try to avoid from using global variables (hence the :=
operator) because maintaining a global variable makes the code harder to reason about.
I read the Action.js
and I noticed the "blazing fast" claim. I'll try the benchmark locally ASAP.
Again, re-created your example (I belive I saved the logic):
data =
s1: false
s2: false
new SignalBranch {
first: !->
my-physical-button.on 'pressed', (button-num) ~>
data.s2 = true
@check!
if something
err, res <~ foo
data.s1 = true
@check!
else
@second!
second: !->
if mydoc
doc = mydoc
data.s1 = true
@check!
else
err, res <~ bar
data.s1 = true
@check!
check: !->
if data.s1 and data.s2
@complete!
complete: !->
# result will be delayed till the button is pressed
}
Oke, ive got the point - We use what We use. 8)
You may avoid :=
by putting the data into generic object, so,
the assignment will be data.propertyName = value
.
Also, it's not neccessary to be global
- just some upper scope.
As far as I could understand,
async/await
is there for adding syntactic sugar that makes the code that heavily usesPromise
s readable.LiveScript has backcalls. We can write thousand of callbacks in the same column, no callback hell. Backcalls are like "Just replace
=
with<-
and you now haveawait
".Is
async/await
a solution to a problem in the Javascript world that Livescript has solved it before? Isasync/await
pointless in Livescript?