I'm dumb, how does core async make sense on the front end at all where there is no way to apply back pressure across the network, or really much use for dropping events in the que? I feel like it makes sense only when you're managing the que/channel, if your just pulling ever item off the channel as soon as it arrives, it feels like ...idk, does this rambling make sense? What was the justification for the port to clojure script? Again, I'm not clever, i feel like I'm missing something or not working on the right problem to see the silver lining.
I think you're focusing too much on what you can do to a queue, instead of what a queue enables you to do. When Rich et al say "queues are awesome, and you should use them more", I don't think they mean queues are good because you can slide over them or reorder them. I think they mean queues are good because you can put them in-between processes, and suddenly those processes don't need to know anything about each other, other than what's on the queue.
The queue is primarily a decoupling mechanism. It's a way to enable CSP; the important part of CSP is the processes, and the fact that they are sequential; in order to achieve that, they communicate through queues.
core.async will be invaluable in any frontend work where you conceptually have multiple streams of work you want to happen concurrently, because it will let you structure your code so each of these stream is a single, sequential code block. In other words, go is at least as important as chan.
Didn't read all this, but worth a mention that core.async cljs predates async/await, and this is an interesting article about generators/csp: https://swannodette.github.io/2013/08/24/es6-generators-and-csp/
and I recognize this maintainer but I guess it's not widely used https://github.com/jlongster/js-csp
Empirically, I guess CSP hasn't made much sense in JS land?
I wonder if you can just build some channel-like construct on top of async-await these days, eg: https://docs.rs/tokio/latest/tokio/sync/oneshot/index.html
> core.async cljs predates async/await Yes
Well, certainly
A ton of people have built libs no one is using
CSP is ultimately built on top of callbacks and state machines, so yes; Without macros though it will be quite awkward (not saying the macro approach is ideal)
async/await is IOC, channels are not that special
> A ton of people have built libs no one is using Wisdom of the crowd is not always no.1
As long as it works and solves a problem, it doesnāt have to be the most popular option
It's not the simplest option either
When this popular/not popular argument comes up, I always remember this https://paulgraham.com/avg.html
> ⦠If they didnāt know what language our software was written in, or didnāt care, I wanted to keep it that way.
> Itās not the simplest option either Thatās about that specific implementation or CSP in general?
CSP in general, for the frontend use-case
It just doesn't solve a problem anyone has
except the problem of wanting to use CSP for frontend
Iāve elaborated above but it does solve problems when the problem is not super trivial
I guess it's a self-consistent framework for functions like throttle?
With only one concurrent source of events, perhaps. But even then, they are concurrent, so effectively each event is independent. Can you reason about order implicitly in JS because thereās typically only one thread? Yeah, but thatās hard and complex. If you care about order, and you only have callbacks/async/await, the order problem is on you, without something like CSP. Not to mention if you need to start computing aggregates, filtering or dropping events, etc.
I think channels with transducers is an underutilized power we have in Clojure. I.e. (chan 42 (map inc))
> I guess itās a self-consistent framework for functions like throttle? Framework is overstating how low-level it is⦠Perhaps flow is closer to a framework or āarchitectureā https://clojure.github.io/core.async/flow.html
Hm, it's not a framework, but it creates a function-coloring problem, so async will make everything that touches it need to also run in async blocks.
There's no blocking-get and put in cljs async, right?
I think thatās the hard part of using it: there are many edge cases to know and think about, but thatās not saying much beyond āconcurrent programming is hardā. But I do believe it can lead to a simpler system; without CSP, everyone ends up re-implementing it via stateful callbacks/async/await
> Thereās no blocking-get and put in cljs async, right? No
coloring⦠In that sense, yeah⦠specifically in CLJS
in practice, I donāt think thatās much of a problem, since often the end result of the whole async ceremony ends up in some atom somewhere; so thatās the escape hatch
Say, an atom that drives React state, most typically
There was another thread in here, my interpretation of that is JVM and virtual threads basically deprecate go-blocks. So what's left is queues and backpressure?
I think it's because, you can kind of sprinkle promises as you need it, and often times, it's not even you that cares, it's just the API returns a promise. CSP requires you to start and think, maybe I can model this using concurrent processes... And that's more of a buy-in from the start.
> There was another thread in here, my interpretation of that is JVM and virtual threads basically deprecate go-blocks. So whatās left is queues and backpressure? In a way, yes! even though technically, the macro go-blocks are staying
But they have less utility, effectively JVM is doing that work instead, for most cases; Still some gotchas around VirtualThread but I think they are getting quite solid now (not much experience with them, yet)
We had no trouble switching a bunch of stuff over to virtual threads. It's magic.
But we're not really using core.async
I said this before, a channel is not a queue. The buffers you can optionally have on them is a sort of queue, if you want to call sliding-buffer or dropping-buffer a queue. But a channel is a synchronization primitive that does rendezvous. Like when you look at it from the abstraction point of view.
It made the profiler output harder to read, so I'm in the process of convincing people to add otel tracing š.
Iāve read posts of companies running into problems at scale, but itās a net win, and I think Oracle is hard at work to eliminate or minimize those cases
OTEL⦠oh man š Have you used it?
Hold that thought. One of the issues I had with virtual threads is that we do a bunch of rust FFI, and that can pin virtual threads. So, I basically built my own async runtime to work around it, although we're not sure we want to go that far. I found out that a one-shot channel is essentially just a mutex, as you say, a locking/sync primitive.
I get the supposed value proposition, but last time I tried to use it, it was the biggest antithesis to the Clojure philosophy of āsimpleā (thatās about OTEL)
I would rather pay NewRelic or similar than deal with OTEL⦠if it fits the budget
I mean you now have literally unreadable stack traces, if you use virtual threads like we do, which admittedly is an overly-complicated way to do it.
new relic can't fix that for you
do virtual threads generate unreadable stack traces?
I thought that was one of the things that they had as an advantage over Netty-style async programming of the past
Every time you create one, it's like submitting a task to an executor, so you cut your stack off and can't stitch it back together, eg if an exception raises.
As in, you could achieve the same scaling gains of not hogging a thread via Netty, but stacktraces would be bizarre
Hmmā¦
But at least whatever is on the virtualthread does have a normal-looking stack trace?
Isnāt that good enough?
With a little restraint it could be good enough, not sure we have it š
There's a specific pattern we're doing using 'with-open' that will spin up a virtual thread per resource, and there's a lot of those.
Thatās like saying that when you call āfutureā you get cut offā¦
I guess itās⦠expected? š
@(future
(throw (ex-info "i throw in the future" {})))it grew out of this and it's frankly gotten out of hand: https://medium.com/@maciekszajna/reloaded-workflow-out-of-the-box-be6b5f38ea98
before we did it with virtual threads, the stacktraces were unreadable because they were too long, now they're empty
ah⦠hmm
Iāve been experimenting with something like this:
(defmacro try-io! [& exprs]
`(try
(do ~@exprs)
(catch Throwable e#
(timbre/warn
(ex-info "IO failed" {:code-executed '(do ~@exprs)} e#)))))
Itās not a standard solution, but Iāve been enjoying it, at the REPL/during dev at least
(try-io!
(slurp "i-dont-exist"))java.io.FileNotFoundException: i-dont-exist (No such file or directory)
clojure.lang.ExceptionInfo: IO failed
code-executed: (do ^{:line 2, :column 3} (slurp "i-dont-exist"))Who needs stack traces, when I can see the code that ran and failed lol
It is a nice API for expressing complex asynchronous code, regardless of back-pressure considerations. AFAICT, JavaScript runtimes (both browser and Node.js) sweep the concept of back pressure under the carpet. Because everything is asynchronous, you never really know when youāre going to run out of resources on that single thread. Itās like a no-limit credit card. In reality, there is a limit, but you never know when itās going to come into effect. In practice, for JS runtimes, thatās not much of a problem. Would I personally prefer to use such a runtime for data processing when it matters? No.
Isn't there only one thread in js?
Yes, by default, unless youāre using workers. But thatās almost like another process.
So, yes.
What do you consider complex async work?
i feel like core async is all about ques, when have you managed the que in front end work?
An animation š
Notice the use of alts! and <! everywhere
alts! is useful here because the animation is cancelable by the user throughout, on any step.
Thatās just one example, can be coordinating requests between multiple services, calling 5 different APIs, and processing the first one that responds first, etc, etc
Many use cases
What's the user experience those alts are providing?
All theoretically possible with promises, or callbacks, itās just a question of how readable the code is.
> Whatās the user experience those alts are providing? An animation starts on a mobile phone, thatās full screen
alts! cancels the animation on any user touch, via interrupt-ch
Notice the interrup-ch is a promise-chan , so it receives input once, and all takers from that channel receive that input
itās relatively complex, but the code reads linearly
With promises, or callbacks, it would be some stateful nested mess
With a bunch of if/else everywhere
The animation just count ā3ā, ā2", ā1ā, āGo!ā
You can see the numbers clearly in the code, the countdown
Hmmm.
If you have not used alts! might be hard to make sense of the code
Interesting
Or promise-chan ⦠both very powerful primitives that are hard to replicate with promises, etc
I'm not sure it would be less readable as a series of if blocks, in a way that would be more clear what the path is. What happens if a user interrupts at 2?
The whole animation stops
That would be line 62
I mean, how do i learn that reading the code?
Well, you need to know how the overall system is wired up, just like any other system; but I have not looked at this code in years, and I remember š
If user interrupts at two (by touching the screen): a message is put! on interrupt-ch , which line 62 will receive, and then the code will execute all the way through instantly at all other steps⦠because interrupt-ch is a promise-ch
Notice, at line 42 we save interrupt-ch in a global mutable state⦠that is centralized for the whole app.
Gotcha.
So some other code somewhere else puts values on it
Fundamentally, core.async is stateful, but itās well managed state
Why is it an alt here? What's the alternative to the user event?
(timeout ...) meaning that as far as the go block is concerned, weāre āparkedā, aka stopped. But obviously, no actual thread blocking.
While weāre āparkedā, bounce-f is running
Hmmm.
Which is actually running an animation
But that is a separate system from the go block, it actually runs in native code⦠(itās a mobile app)
Is this a common thing? A way to control/encode timing?
So the animation does not get blocked, alts! just makes it ācancelableā by allowing us to āunparkā at any time, and remove the view, aka ācancelā the animation
I donāt know
Thatās how I thought to do it, and worked well for my case
The most barebones one to do, is to use setTimeout
thatās the most primitive construct to āwaitā
At least you're using the que
Basically, schedule something to happen in n milliseconds
But how do you now decide not to run that?
Some global mutable variable, that gets checked before execution? Maybe
Again, thereās nothing fundamentally novel here that is not possible in raw JavaScript
Itās just a question of clarity, and a bit more understandable code by using managed/structured concurrency primitives
The penalty is increased bundle size: that kind of async code does get compiled to⦠quite a lot of JavaScript
If thatās not a problem for you, then I generally think core.async is great for JavaScript
Thatās most clear argument Iāve heard against it
My dumb brain just goes back to: i never manage a que of events in js.
You get a http response, you deal with it. You don't get 50 and decided to drop the last 10.
Most other arguments boil down to: āI donāt like itā, āhard to understandā, or āthereās bugsā; I would say most bugs are fixed; Hard to understand? Sure, itās a different way of thinking about program structure, but thatās CSP, not core.asyncās fault.
> You get a http response, you deal with it. You donāt get 50 and decided to drop the last 10. And that might be fine!
If your problem is relatively trivial, like youāre saying: just one response, no problem! Callback, and call it a day š
Now⦠what happens when we need add retry logic though
How do we do that with callbacks?
More ⦠callbacks? š
Perhaps it doesnāt matter, we just expect the user to click again, or we display some error.
Depends on the use case, always
Otherwise we can talk abstract all day
Yes, a different callback
How do we know how many times weāve retried?
(if weāre talking about retries)
Perhaps use some library⦠that manages it for us, someone else has written the stateful callbacks š
I guess I'm missing how having a que is helping with that...
With core.async, it can be a loop/recur, that has n number, so it looks like idiomatic Clojure code
One very powerful way to use core.async, for an important part of a frontend app, is to put all requests that come from the network on a channel
That way, we know what order the requests came in
Sometimes that can be important, or convenient
I.e. letās say we need to send request to some analytics service, and register a user at the same time, but we should not proceed with login, before the analytics service responds they got the user info
That would matter if your going to change the order of events in that que.
Anytime order matters
Or when we need to wait, a bit
And āsynchronizeā between different events
With callbacks, any one of those things typically ends up being a little stateful island in the app⦠And more often than not they are all ugly and done in a slightly different way; again with core.async, you can design one unified way that is sane and makes sense
Can you design an ugly system with core.async? Sure
But at least gives you the right primitives to design a good one! haha
Itās like a nice set of paint brushes
But it doesnāt solve the problems for you, itās not a framework, itās a library
I'm trying to think of a time i have had a fe application call multiple end points at the same time and it was responsible for making sure the logic was ordered. I feel like that kind of strictness always gets pushed into the persistance layer, which is why the frontend is thought of as a view.
Typically registration logic, combined with analytics
i don't follow.
Can be a hairy bit, combined with writing some persistence into the browser, SQLite, etc
Registration logic/UI for a frontend app is where you need to call multiple endpoints
And the calls often have to be ordered
Assuming the frontend is calling some analytics service
So your FE is stitching together data? Thats what i mean, that sounds like a recipe for disaster.
if that info needed to be atomic, it needed to be so at the persisted layer.
Your frontend needs to call Google Analytics
Itās a very common thing people do
Can you call that from backend? Sure
But often people want to call it from frontend also, to detect any potential problems, i.e. say if frontends sends the event to Google Analytics, but the backend does not, something is broken
I'm not tracking how core async plays a role in that though...
One potential way it might work:
1. Call Google Analytics to report Guest visit event
2. Register call to backend (BE) -> get user-UUID
3. Call Google Analytics to report Register event with user-UUID
4. Login call to BE -> get login-UUID
5. Call Google Analytics to report Login event with login-UUID
FE stitching data? yes, how else would you do it, assuming you want to track each one of those steps
If your frontend is literally just a view, then it does not matter, correct
I mean, those are discrete events, i'm making the claim, core async is useful when your dealing with a collection of events as a collection. What you have is a list of handling each event at once. I'm not seeing how coreasync plays a useful role in this.
Well, if you donāt see a problem with the way youāre dealing with async state, then thereās no problem, really!
You started on the premise that core.async is only useful for managing backpressure on the server: thereās other ways to manage backpressure on the JVM, like any queue primitive, those all provide backpressure
> a collection of events as a collection. The events are related, if thatās what ācollectionā means here, but Iāll stop the discussion here, because I am not sure I can give better examples š
related = they share data
ok, you have 5 things in your que, what are you doing with it? is sliding? are you dropping some? are you waiting until you get 3 before you take some action?
thats what i'm assuming a que would be useful for.
Yes! You can do any of those things!
so you would drop 5 (call GA to report login) if the que size was 4?
Again, it all depends on the use case, but dropping is a very useful primitive, yes, for some cases
Not in this case, that would not make sense⦠letās say itās a browser app that tracks body pose coordinates
Sends me 100s of events, but I only care about the last 10
stuff like that https://storage.googleapis.com/tfjs-models/demos/posenet/camera.html
it sounds like the use cases youāve worked on just donāt need advanced logic for async handling, and thatās perfectly ok
I think the deck is stacked against core async here because the FE is a view for the user, not a processing house, so the propensity is to only be sending things to the view that can... be viewed. So that means the BE has already curated those events in some order that makes sense.
That might be the case
Some people donāt even want to have a backend
Local first, etc etc
Itās a minority
But it exists
Iām just saying the world views are broad, and there are always inherently complex use cases š
That core.async can help with
I agree, if the browser app wasn't a "view" the queing capabilities would start to make sense.
Backend does not make things inherently less messy, assuming it still need to do all that coordination, youāre not gaining anything by first shipping all the data 100s of miles away, thatās only adding complexity
(Thatās the Local First view point, if I can dare speak for them, even though Iām not super involved with the overall approach, at the moment)
i assume local first is the notion of trying to do more on the client?
All
Not āmoreā
so "desktop apps" 2.0
https://www.localfirstconf.com (a conference! š )
(no association on my end, just found randomly)
Smart people have written papers on it, itās a thing https://martin.kleppmann.com/papers/local-first.pdf
I do like the idea, in the abstract sense
CRDTs
good chat, ttyl
Yeah, martin is a hero of mine. I can't believe i got to chat with him a couple times, real chill fella.
Well, then you better know about Local First! š
I have the extremely vague impression the real issue with CRDTS is users are trained on the centralized experience.
One of the most clear explainers of CRDTs
I donāt think itās that
I think that problem is that itās economically hard to build things that are truly Local First⦠But, examples exist; Obsidian?
Itās not āstrictlyā local-first but it comes close
Also, CRDTs are used commercially also, for example Apple Notes
syncs via CRDTs, almost 100% guaranteed (and the sync is damn good)
I think thereās enormous potential in them, even as a server-to-server sync primitive, or even server-client
The good thing is that CRDTs donāt care if itās local-first, server-first, etc
CRDTs are also most know for the text syncing algos, which are the hardest, and also a limited use case
There are arguably simpler CRDTs, for syncing data structures
Also, the reality is that many apps donāt truly benefit from CRDTs, if the user does not care about Local First/privacy/etc
Again, I think they can be widely useful, and are underused as a cross-server syncing primitive, server-client syncing, etc
Honestly, what i tend to figure out is that if you go down the unconventional path, you end up right where you started, only wiser, but also a bit more worn down lol. Usually the reason things are they way they are is because gravity pulls them there, you can change the units, the universe remains. The big benefit is that it often lets the person who understands the ins and outs to see past the choices that don't matter, to the few that really do.
Put another way, i would start by looking at where CRDTS and local-first is already flourishing....
> only wiser, but also a bit more worn down lol. Haha I agree with wiser, but worn down does not sound very optimistic š
> Put another way, i would start by looking at where CRDTS and local-first is already flourishing.... That makes sense, and it aligns with my view of āconventionalā places, not strictly ālocal firstā
server-client, server-server sync
> if you go down the unconventional path, you end up right where you started Well, arguably, Clojure is a counter example of that š Before Clojure, nobody was really using persistent collections in a production environment, with good performance; many people thought itās not possible to do
Thatās, I think, a not often discussed point about Clojure, perhaps out of humility, but in my opnion, it was a very significant innovation
persistent immutable collection, to be specific
sure. It's a nice tool, i'm here and have been for years. It's a bit of an illusion though, it's mutating under the hood. That's what i'm getting at, you usually find the true distance between competing ideas is much closer then it appears verbally.
hahahā¦. well come on, sure, but yeah thatās were the rubber meets the road, yes
If you went all theoretical and pure functional
For all intents and purposes, itās pure, and functional, from the userās perspective
Agreed. I came to clojure because it was the right tool for me at the right time in my journey. It clarified everything i was trying to understand before and then offered conventions around issues i was confused about.
For pure functional stuff to be fast, we probably need fundamentally different hardware; thatās a different level innovation, perhaps not for this century lol
Right, i tell people clojure is well suited for network bound computation. (is that the right way to say that?)
if your system needs to run as fast as it can on a single machine, then you should probably start lower (c, rust, etc..)
Thatās ⦠debatable; but not going to have that debate right now! š
me neither. I'm going to fall back asleep. Thanks again.
> What was the justification for the port to clojure script? Javascript also runs on the backend, in mobile apps, and desktop applications (like slack clients)! > i feel like core async is all about ques, when have you managed the que in front end work? Core.async has queues and they are an important part of the design, but I wouldn't say that core.async is about queues. Core.async shines in scenarios where you have multiple asynchronous inputs and outputs to your program. This happens all the time on the frontend where you have an unpredictable user, multiple unpredictable network connections, timeouts, and other asynchronous apis (workers, animations). > there is no way to apply back pressure across the network, or really much use for dropping events in the que There are multiple methods for applying backpressure across the network from the browser. There are also other inputs and outputs besides the network inside a javascript program where you may want to apply backpressure (eg. specifically on the front-end, you may want to disable user inputs to keep the user from initiating too many requests). ---- Another topic that wasn't really mentioned is dealing with timeouts. I find core.aync's timeout support very elegant compared to other async libraries.
> My dumb brain just goes back to: i never manage a que of events in js.
For some events, like mousemove isn't it pretty easy to imagine wanting a sliding buffer? Like you want to keep current mouse coords in some state atom that also tracks text of an input field. You could always manage this with callbacks closing over state, but I think if you were to try to extract the essential abstractions so as not do this in an ad-hoc way each time, it be hard to end up with something better than channels+buffers.
Another simple example is a search bar where you can show results as you type. It's common to have requirements like: ⢠don't send a request for every key stroke ⢠update periodically ⢠only have one request in flight a time (but timeout if it takes too long) ⢠debounce the initial key type events
But it's not really so much about the queue as it is about dealing with multiple asynchronous inputs and outputs. It's trivial to implement a queue in javascript.
> Core.async has queues and they are an important part of the design, but I wouldn't say that core.async is about queues. Core.async shines in scenarios where you have multiple asynchronous inputs and outputs to your program. This happens all the time on the frontend where you have an unpredictable user, multiple unpredictable network connections, timeouts, and other asynchronous apis (workers, animations). Multile async inputs and ouputs...that you put on the queue that you manage?
can you give me a code example? are you talking about using the alts function?
> There are multiple methods for applying backpressure across the network from the browser. Yeah, you request less often right? what else would there be (unless were talking sockets maybe). But thats not a feature of coreasync right?_
> Multile async inputs and ouputs...that you put on the queue that you manage? Yep!
Queues are an important part of the design.
Yeah, you request less often right? what else would there be (unless were talking sockets maybe). But thats not a feature of coreasync right?_Sockets, long polling, web rtc, http requests, media players, image loading. These aren't features of core.async, but core.async can help you coordinate them. I was responding to "there is no way to apply back pressure across the network".
focusing on channels (especially as simple queues) as the value prop of core.async disregards the power of decomposing subsystems into processes that communicate with one another. I posted a few links above which I believe are good examples of this kind of power.
I don't usually recommend core.async because it can be difficult to learn. It's also difficult to apply because managing multiple asynchronous sources/sinks is inherently complex. However, if you do take the time to learn it, it's quite powerful.
@joe.lane i'm reading... I think my confusion is how to judge if this is a good use of a tool like coreasync, or just over engineered, whats the critera? Here is my naive answer to that question, the logic should need to manage the queue, as in dropping-buffer or sliding buffer or maybe alts.
At least in my experience, everything goes wrong the first couple times you try to use core.async and it's very frustrating.
it refers sliding-buffer... then doesn't use it https://github.com/bhauman/dotsters/blob/master/src/dots/core.cljs š
Personally I find the higher level core.async functions like a/merge/`a/transduce`/`a/into`/`a/map` really nice to work with, and I like how they work just as well with promise style channels (including the result of a go block) and queue style channels, and make switching between these styles and composing them pretty nice. I agree there's definitely a learning curve, but having got passed it myself I haven't looked back.
@smith.adriane I wonāt defend the onboarding experience, it can be rough, but if you have any feedback you think that would ease this frustration Iām all ears.
@joe.lane It's too late. I learned it years ago. I think a lot of the difficulty is the inherent complexity of building asynchronous systems. Maybe a guide that combines the wisdom of CSP and Making reliable distributed systems in the presence of software errors with core.async examples?
And maybe Java Concurrency in Practice for using core.async on the jvm.
Good luck! š«”
@joe.lane why is dotstar a good example? My criteria, as stated above, was that it would need to manage the queue itself, that, to me, meant using sliding-buffer, dropping-buffer, maybe alts, (their might more queue managing functions) because that implies it's actually _using a queue instead of a over-engineered promise, that example, at a very quick glance doesn't use those functions.
or we can change the criteria, but it needs to fit into some structural framework.
i guess the a/merge a/map, etc.. functions would also count.
i haven't thought about what those imply...
what's wrong with "Core.async shines in scenarios where you have multiple asynchronous inputs and outputs to your program."?
define "shines"
excels
thrives
Merriam Webster offers "to be distinguishable by superiority : surpass others"
imagine someone said "how do i deal with multiple asynchronous inputs" and I said "use something that excels"
would they end up using core async based off that? I don't think it would help them point the way. What i'm saying is that i'm either framing my problem wrong, or the tool that reportorial helps with it.
If i needed queueing logic, i fully understand core async would be useful, i'm trying to understand when thats happening to people though, or if they are just pushing queues into scenarios that don't need them at all.
> how do i deal with multiple asynchronous inputs I would ask multiple follow up questions as well.
I think that response is a bit disingenuous Drew, can we stop with the quibbling of words and their definitions before it drives people away from this conversation?
@joe.lane i was responding to @smith.adriane
he asked "whats wrong with"
If someone asked me how to make a web app, I might suggest javascript, but I don't know how to give a short answer to that question.
I know. Iām asking you not to respond that way to them.
I think it's best to think of core.async a bit like recursion vs loops. Or for vs reduce and so on. It's a different abstraction for modeling concurrent behavior. It's not necessary to have it in JS, because JS already has abstractions to model concurrency, but it enables a choice where some might prefer the core.async abstraction in some situations
be more specific, what did i say that your having an issue with? i'm trying to give an intuition that his remark wasn't going to win people over. You can't simply claim your tool "shines/is-good" you have to show how it fits a problem the user is having.
can you be more specific*
I'm not trying to win people over. I typically don't recommend core.async because there is a significant learning curve that most people aren't interested in.
Having built many asynchronous systems in the browser and on the JVM, I have found core.async to be in invaluable tool. I've also tried recommending it before and have seen folks struggle to get over the hump.
One clear difference is that core.async is based of a stream of values. Whereas generally promise based approaches like in JS are based on single element values. Another difference is that the async stream support in JavaScript is forceful, there's no handshake. So if you need handshakes core.async is nicer.
@didibus my extremely naive question is "what is someone doing with that queue of events" because in my mind, you would need to be using the sliding-buffer or dropping-buffer, otherwise your ... processing every element.
But fundamentally, you need to have a problem where you have concurrent processes, and they need to be sequenced. The "sequencing" means they depend on each other at certain points where they have to wait and get something or communicate something to the other to coordinate themselves concurrently.
> Another difference is that the async stream support in JavaScript is forceful, there's no handshake. So if you need handshakes core.async is nicer. i'm thinking about this... what do you mean by "forceful"?
@joe.lane backing up, thanks for your input. I wasn't attempting to be dis-respectful, but the question was about why a particular phrase wasn't good enough and to re-iterate, it's because, in that case, the phrase was putting all the heavy lifting on a word "shines" which doesn't point in a meaningful direction. It doesn't lead to good follow up questions. @didibus can core async help you create a dependency order on events?
> my extremely naive question is "what is someone doing with that queue of events" because in my mind, you would need to be using the sliding-buffer or dropping-buffer, otherwise your ... processing every element. I use sliding buffers all the time, for the reason they are advertised for; whenever I only care about the most recent event. Examples mentioned earlier are keeping currnet mouse coords from a bunch of mousemove events, or a typeahead search bar where a new keyup event makes the previous string query results unnecessary. Are you saying it's strictly better to implement these things without core.async? I know core async isn't necessary to implement them, but the fact that some people prefer these abstractions should justify its existence on cljs no?
> I use sliding buffers all the time, for the reason they are advertised for; whenever I only care about the most recent event. recent event (1) or events (1+, but less then the everything)?
In those examples, (1)
why not an atom then?
i'm being honest, i feel like im missing something critical in this.
The point of framing core.async the way I did is that it's better to focus on describing and understanding the problem you are trying to solve rather than particular functions that a library has. Core.async can be applied to a wide variety of problems.
Most of your counter examples are focused on the "what" rather then "if" and "when".
When an event is processed or if an event is processed is the hard part of the problem that core.async helps with.
To be more concrete, try to implement the examples from https://github.com/swannodette/async-tests without core.async.
@smith.adriane thanks, ill look this over.
david nolen has a bunch of concrete examples
why not an atom then?⢠Swapping a shared atom for every mouse move when that state is shared with other things that swap it feels kinda gross when the mousemove will happen 100x more than the other events that change that state, even when there's not really any contention risk in cljs. ⢠idk I just feel I've built up intuition over time that I like how my programs shake out better when I use these abstractions. I can easily change the buffering strategy, buffer size, I can add stuff more stuff to the channel. Don't really expect this to convince anyone but to me it's just a good abstraction that I want to use where it fits, which to me is stuff like this ⢠EDIT: also these are one component of a larger app and at some point you want to coordinate with other things in the app and it's nice to not have to think very hard about how to do that each time
actually, I'm not sure which of these are the best examples, but you can check his old blog posts.
From another angle, do you have a chunk of code that does something decently non trivial of the category we're talking about that you feel could definitely could only be made worse by using core async?
@jjttjj you would reset! not swap for this case right? My intuition is an atom would reset! about as fast as a dropping-buffer with size 1. > From another angle, do you have a chunk of code that does something decently non trivial of the category we're talking about that you feel could definitely could only be made worse by using core async? My contention, or hypothesis, is that any interaction that could be sufficiently modeled without a queue management system would be made overly complex by adding one. The way Rich, and to some extend david, talk about core async leads me to believe they have found a way to model most systems with queues, which means they manage buffers (sliding, dropping ,etc...) Im having trouble, because of my past experience seeing front end browser work in this light and it makes me feel like there is some major "ah ha" moment. Meanwhile, a tool like re-frame, or odoyule rules, instantly speaks to the types of problems i run into in my browser work.
it's not so much "core async isn't good at something" it's that some of the literature gives me the impression i'm working in a very different context, is that because of other choices i made or because i'm not viewing a larger category of things in the same light?
I think someone above said they thought part of the issue was that the javascript runtime doesn't really give you any impression of how limited your resources are, so how are you going to go about setting a mechanism to know when to drop things in the buffer? This, plus the browser mostly being a view, as opposed to a "local first application, which persists data to the users computer" means the browser is often getting a curated, pre-ordered set of events. The notion of being streamed a bunch of things, some of which it might have to drop in favor of others, is foreign.
The move mouse example almost seems to fit, but if your only keep the most current, thats what ratoms/atoms do anyway yeah? Or i'm i wrong in this?
https://jackschaedler.github.io/goya/ https://github.com/jackschaedler/goya Another example of a larger application decomposed using core.async. Not an endorsement necessarily, but you can see the different Om components are built independently, some having their own "processes", and being connected via channels.
thanks @joe.lane!
Ok sorry, (looking after my kid as we chat haha). I fact checked myself cause I had forgotten a bit. At first you have Promise vs Channel. A promise is like a Channel with a buffer of 1 that closes after putting a single item. In fact core.async has a channel that mimics a Promise called promise-chan. But fundamentally. Promise is one value at a time. The async process is assumed to be done after that one value is delivered. A channel is fundamentally a possibly infinite stream of values. It's not assumed that the async process is done after producing one value. Now reasoning about what the material and practical and concrete implications of this difference is hard to be honest. Now JS also has await for... Or really it has async iterators. An async iterator isn't like a channel. An async iterator captures a computation and has a next() that returns a Promise. The consumer calls next() on it when it's ready, and gets a promise, then awaits the promise. It's like get me the next value when you have it I'll wait. A channel is decoupled from any one computation. That's why more than one process can put and take from the same one or even to multiple making it a many-to-many. Another difference is the async iterator isn't a buffer, a channel can optionally buffer things. So you can control 1 element at a time, 10, etc.
I want to add something, a Channel isn't a queue. Only if a channel has a buffer is it queueing. A channel is a handshake between two processes. If you put on a channel that has no buffer, the put cannot happen, nothing to put into (no queue). So the producer waits for a consumer to show up. Once a consumer shows up he doesn't take from a queue, he takes from the producer directly (the producer gives it to the consumer). It's a synchronization primitive.
If we go back to the async iterator. That's also a synchronization primitive. The consumer can say, I'm waiting for the next task. Now when the producer shows up, the consumer is ready for them. With a channel, it's the other way around, the producer says I'm ready to publish and waiting for a consumer to show up.
Thanks a lot, that makes sense. I have a lot to think about.
Well, it can go both ways with a channel. The consumer can wait for any producer to put on the channel. That's why they say it's a rendezvous
That's the thing, it's different abstractions, different semantics, but a lot of similarities as well. It's hard to go from that to like, ok so in practice what does it change? When to use which?
That's why I was saying it's a bit like for comprehension vs map/reduce. Or loop vs recursion.
I think some places the CSP model is good at: ⢠Two or more concurrent processes that take turns waiting for each other ⢠Complex conveyor belt style processing where items might fan-out/in, go through multiple stages, etc. ⢠Stream handling and buffering
Thanks didibus. I learned a lot from everyone that chimed in. I'm going to go wrestle with all those ideas and hopefully come out the other end stronger. Sorry if i can on strong early on, i was trying to cut into what confused me and that's always hard to communicate.
panics are more like exceptions but discouraged from use. I was speaking about the Result<T, E> type and different ways to compose an Error type, which is a normal returned value. It's pretty normal to use an enum where each variant might be another enum type, like http error wrapping a json deserialization library error.
The library author can't make a decision for all the possible applications that might use it on whether an error is recoverable or not.
The caller can pattern match on whatever is interesting to them, it's more like checked exceptions but with better tooling. The compiler forces you to do something intentional with the error type
Common things to do are: ⢠just pass it back up the stack to the next caller ⢠wrap the error in another type ⢠wrap the error in a dynamic error type (anyhow), similar to how golang does it, if you are sure the caller won't need the details (although it can still dynamically downcast to get the underlying error type, you don't have the compiler helping you like you would with a pattern-match) ⢠create a panic! and crash the program, if you must
Library code should never call panic itself, application code can but it's kind of a bad idea.
One notable counter-example is that anything that does a malloc can panic (if you run out of memory), and that bothers some people, but it might be worse to put that error into essentially every function's return type, eg if it does something like using strings or vecs, or calls another function that does.
On the JVM, you can catch an OutOfMemory exception if you want, but you likely shouldn't do that. You can catch panics in rust, too, which is also probably a really bad idea.
So, async rust doesn't really deal in exceptions, just returned values that might wrap error types like Result<>. Heterogeneous collections are hard to do, so you can't just do anything like throw 'nil' on a channel. I agree it's weird that core.async decided to use nil that way, and I also thought it wasn't clear how to do exceptions with it when I tried ~10 years ago, before I even got into all the typed FP.
I thought the ocaml monitors were a superior way to do async exceptions, and I think I like using a language that basically avoids exceptions entirely even more.
Does Rust not have nil?
The Error trait seems the same as polymorphic nil to me.
safe code uses Option types, unsafe code can do anything C can do, so yea.
yea, generally you wouldn't use a None to convey an error when you can have a richer type with more details.
Safe code you're not allowed to use nil? Or it's more a best practice?
hm, there's no nil. But pointer derefs are just operations on numbers and nil is basically 0 in that context.
Which would still error if trying for a method lookup?
Ok, so say a function wants to take optional arguments? What's the pattern? You sub-in Optional.empty ?
yes, optional positional arguments or a struct with optional fields
So if you lookup a key on a map, and it's not there, you get back an Optional?
yea
you always get back an optional
if it's not there, it's a None
I recently rewatched 'maybe not!' and my take is that in clojure, everything's optional. In a language like rust or ocaml, it's convenient that some things are non-optional
I don't think that addresses the issue of nil in Clojure. The nil in Clojure is much more like Optional already. (The interrop nil is a problem though)
Ya, I find Clojure nil is already like if everything was Optional
And in Clojure "some things are non optional" cannot be enforced. So it's just a doc-string if that's the case and relying on it is trusting the doc string.
Where nil in Clojure I've heard criticism is that it can often be ambiguous.
Like (get {:a nil} :a}
So if you want to give "details", like what's the cause of nil? If the caller can benefit from knowing. If you instead return custom indicators then it breaks the generic nil handling for callers that don't care and it's annoying.
Yeah, I don't like to have to think about the possibility that a nil value in a map means something different than if the key is missing.
I try to avoid that in any codebase I'm on by stripping those out and never associng a nil myself.
I was wondering if you merged nil and errors.
Say you didn't have Optional, only Result and nil None was just a type of error that implements the Error trait ?
Not every function returns a Result type, either, but you could use error types for that, sure. Anything can be an error type.
The value of a shared Option type is just that you don't have 20 option types that different people wrote.
It's just enums and structs at the end of the day
I don't think the trait system would help you hide that optionality in the signatures but keep compiler enforcement, if that's what you're asking
this is a fun one: https://doc.rust-lang.org/std/result/enum.Result.html#method.transpose
So, if a function in Rust returns just a plain string for example? It means it can't error?
yea, correct
minus the panics I mentioned earlier, String does its own heap allocation and something like running out of memory would crash you.
They have a more explicit result-returning function if you care about that https://doc.rust-lang.org/std/string/struct.String.html#method.try_with_capacity
And what happens if something can error and return optional? Result<Optional<T>> ?
I think that's rare, but I found one: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
Haha ok.
Maybe you want a shared error type for all the functions in a module, it would be weird to add a None case that is only used by some of them.
I still wonder what would happen say we had:
nil - same as now
nil/type - a "sub-type" of nil, to specify not-found, missing, contains-nil, etc.
(nil/type message parent) Constructs a nil type that is like an "exception", will capture stack trace, can wrap another nil type and can include a custom nil message.
And they'd all still be nil? true.
In clojure it sounds like you want a nil protocol
You'd need anything that checks nil to call that protocol instead. Effectively you've made ruby nil at that point, I think? It's probably slow.
Ya, in Clojure it would be a protocol. Or some custom thing. And it wouldn't play nice with Java. I'm more in the realm of Clojure 2.0 I think haha. Say no more thrown exceptions (apart from panics like rust). Functions return a result or nil.
I really don't like the way golang does it, but it sounds a bit like that https://go.dev/blog/errors-are-values
It's basically a convention that all functions return a tuple of [value, err] that you have to constantly check
You're not suggesting a tuple, but what's the difference?
if (second-slot) is poor-man's pattern-matching
It's not like that, all functions return either a value or nil. The success path looks just like normal Clojure
In that same talk, Rich seemed to like union types, which might be what you're looking for. I think of them as an enum with unnamed variants.
In that hypothetical, a function returns value | nil , not Option<value>
I used them a handful of times in typescript and I'm not sure they're worth it, personally, but a compiler can verify it
It would be like:
(get {:a 1} :a) ;> 1
(get {} :a} ;> nil/key-not-found
(get {:a nil} :a} ;> nil
(get {} inc) ;> nil/invalid-key["Key must be hashable." [:stack ..., :line ...]]that's all fine, but what do you return out of a function that calls those functions?
And in all cases, you could just do:
(when-let [res (get ... ...)] (* 10 res))
sure, I guess it could return nil/get-or-inc that automatically wraps the other things
It's handled like normal Clojure nil is today
The only difference, is maybe if you wanted to know, ok, why is it nil? Then you could match on it.
Hmm, it seems like exceptions are a way to force something to get handled, and this loses that property
If you don't pass it back out, it gets lost
vs with exceptions, if you don't handle it, someone else has to handle
But nil in Clojure behaves that way. Most functions either "handle it" or return the nil.
Yes but I think you're talking about increasing the scope of that to include more things like actual errors, not the presence or absence of values
I suppose a TCP connection error is the absence of a valid TCP connection š
You'd have to be aware of it a bit like exceptions that returning a new nil or returning the same nil you got is different, since there are many types of nil. But that's similar to today
If you think as nil as nothing. Then ya, (make-tcp-conn ...) ;> nil It's like, I wanted a TCP connection, I got nothing.
(some-> (make-tcp-conn ...)
(connect "http://...")
(send "foo"))And say you want to "handle errors" instead of just short-circuiting and ignoring them:
(if-let [res (some-> (make-tcp-conn ...)
(connect "http://...")
(send "foo"))]
:success
(do (log "Error sending" res)
res))You could come up with wtv convenient syntax, even try/catch could probably work:
(try
(some-> (make-tcp-conn ...)
(connect "http://...")
(send "foo"))
(catch nil/auth-error e
(log e)
e)
(catch nil/something-else e
...))If the try gets a nil? result, it matches it into the catch forms, otherwise it just returns it
> Hmm, it seems like exceptions are a way to force something to get handled, and this loses that property True, this basically defaults all errors to be "nil". And we assume the callers default to handling "nil" as short-circuit and ignore.
But in Clojure.... there's never any real forcing of handling an exception or a nil either. And this would do the same as in Clojure in a sense...
If the nil bubbles up to the top, you see nil/not-found[at line, cause, "Could not foo"] in the REPL
Or if a function didn't propagate the nil it got but replaced it, now you see the replaced one. It's kind of same as exceptions today
So I'm thinking two things... 1. Railway style errors. So errors are returned as values, not thrown. 2. The distinction between nil and error does not matter. The returned value is still just conceptually the same "nothing" with optional details as to why nothing was returned.
I mean it seems easy to unintentionally forget to bubble, and kind of a bad default in that regard. Most apps should have a top level uncaught exn handler that forwards to something like sentry or rollbar
If you see something new there, you usually want to add code to fix it
Ya, that's possible, but I'm not sure. Because if you updated all the core "nil" handling to bubble the nil and not return a new one. Then it might be that you need to manually very rarely make sure you also bubble it back up.
For example (+ 1 nil/not-found) would return the passed-in nil/not-found
Euh, no, actually it would probably wrap it like: nil/value-not-a-number[:line 123, :cause nil/not-found, ...]
You'd even update when and company
(when nil/not-found ...) ;> returns nil/not-foundWould you make a distinction about early exits?
eg a syntax like this could skip passing it into when at all
(when nil/not-found? ...)
What do you mean? This shouldn't execute the body of when, but instead of returning a new nil it would bubble the one it got and return the same one
Let's say some function (not when) doesn't pass what you gave it back out again, and returns a different nil, but you prefer to bubble the first nil
Ah, well technically you can do that yourself, but I would say that function did the wrong thing, unless it did it on purpose.
eg, with get
is there a nil key in the map? does it have to be the same one?
It should wrap it:
(get {:a 1} nil/auth-error) ;> nil/key-not-found[:cause nil/auth-error]Also I'm kind of simplifying the print here. But these capture everything an exception does, the full stack trace, line or the error, etc. So you still are able to trace this all back down to the first nil
you might need some utility to walk the returned nil tree to find things that can wrap nil/auth-error as well as the nil type itself š
when you're trying to match on either:
nil/auth-error
nil/key-not-found[:cause nil/auth-error]
But now we're not even talking about making nil and error the same. More about how error values should work.
Like forget my idea. If you had a Result type.
What happens if someone does:
(get {:a 1} result.error)
Or even just:
(get {:a 1} result)
In static-land, you would just have to unwrap the result to get to the value unless the map keys are also results, before you have anything to pass into get. In hypothetical clojure 2 I have no idea.
Ya, this is Clojure-land š.
I guess maybe thrown exceptions make sense in Clojure for this reason.... You can't accidentally pass as input an exception you received from the previous function.
But I also think, what I suggest here would work. Every function knows that all their input could be "the-value-they-want" or "the-value-they-don't-want" or "nil". And they have to do the right thing in each case. In the case if nil they might want to return it or wrap it.
I like the idea actually, I'm just poking at it from a type-biased perspective
Oh ya, it needs a lot of poking haha. Maybe it's massively flawed somehow. But I do feel there's something interesting in it as well.
One of those biases is if a compiler can verify correctness, it probably should. In this case, if you're matching on a type that can't happen downstream, you probably made a mistake. It should also be knowable what cases can come out of a function and automatically merged into the caller.
Ya, I won't comment on that haha, I hate checked exceptions š
well, 1990s java isn't all there is to say about that, I guess
Wait, is rust smart enough to look at the full call tree? Like if my top-level handles them all, everything below isn't forced to do so?
Rust makes you be explicit, but I could imagine a language that doesn't
Ah, so like 1990s java š
no, eg, ocaml can figure out the types without you putting them on every function signature, and can even generate the module interface files for you
if you just never write them out, that's kind of similar
of course it's sometimes useful to have names on types
Ya, like I'm ok if the compiler is super smart, and is more saying, I'll tally up and union the set of all total type or errors that could happen at every function call (including from all the function calls that function makes). And then I'll force you somewhere in the entire call tree to handle all of them so none of them "escapes".
And then, if it wants to hook into the IDE so the IDE on hover can show you what is the union of them the compiler analyzed
But if it forces the direct parent caller to have to add a bunch of declarations of it's own, and to explicitly "rethrow" them if it does not want to handle them. I hate that
yes, exactly, I could see a caller filtering out some of the cases from the things it calls or grouping them differently, idk
but the default would just be what we're already doing flying blind in a dynamic language
hopefully a little better than that
But I think the issue is, if you "return" an error, how can it "default handle it". Say:
(defn foo []
(let [b (bar ...)] ;; Bar can return 3 types of errors
(bazz b)) ;; Bazz can return 2 types of errors
In the thrown exception style, when bar errors, it throws, and the runtime can intercept that, then do a default like short-circtuit foo and just re-throw the error it got from bar.
But in the error return style, the runtime cannot intercept that the returned value of bar was an error and automatically short-circuit foo and have it return the error it got from bar. (or maybe it could š¤)
So now the user has to do something about the fact that b contains an error value. Logically it does not make sense to call bazz with an error value, so I guess Rust forces you to handle it and implement the short-circuiting behavior yourself.
And I think what I'm thinking is, what if bazz had no issue being called with an error value? Just as in Clojure bazz should anticipate being called with nil value, here too it would anticipate that and just do the right thing . So now the user doesn't need to care about custom handling the error inside foo, it can just pass it along.Functions can be given narrower inputs than what they take. If a function can't handle those nils, then you probably made a mistake and should be forced to do something else like early-return or replace with a default?
Not really, because "If a function can't handle those nils" isn't true. A function can always handle a nil, it might just do so by returning an error.
Right, I think your example is the nuance I asked earlier. Do you want foo to return 2 or 3 (assuming 2 of the 3 are the same)? Does it always call bazz? Exceptions are also about control flow.
What if bazz does a bunch of side effects before returning the error, you can want those to happen or not
If foo cares to handle the 3 errors of bar. Then it could match on b. If it doesn't care to handle them, then it doesn't need to do anything.
> What if bazz does a bunch of side effects before returning the error, you can want those to happen or not
That's bazz's problem though. It get garbage input, why does it go and do side-effects before validating?
Hm, motivating example, do you want jetty not to start if your ring handler is broken?
The thing is, when you look at it from a static-lens. I agree with you. Bazz has "validated" itself by declaring it's argument types to be only what is valid. But in a dynamic-lens, I think it's different.
Hm, motivating example, do you want jetty not to start if your ring handler is broken?It would be more like, should jetty start if you gave it an error as input when calling init?
What should bazz do if called with nil? What should it do if called with an error ?
Hum.. actually maybe this is where the distinction between an error and nil could matter.
bazz might want to handle the nil by subbing some defaults and still working. But it might want to handle error by returning a wrapped error or something like that š¤
my take is that in Clojure everything is optionalThatās not really true. Sure, thereās no Type Oracle to tell me that, but if a map, for example, passes through a spec, thereās a whole bunch of things that I know about that map. I.e., if I check that all values are numbers, all ādownstreamā code can safely sum all map values without worrying about
nil, MaybeNumber, MaybeSheep, or whatever (excluding the OutOfMemory cases, which I think are effectively out of scope, for most languages or type systems).A useful distinction in my mind is specs/schema/malli are runtime checks by all the consumer of the data structure. Static types are at the producer, you can't call the function with the wrong thing.
You still can, you simply can't call it with the wrong types. But there's still plenty of ways to call it with the wrong values or references or with its required context missing, etc.
But you're right though. I think what you bring up is the important distinction. For correctness you can put a runtime assert and then you can safely assume downstream you'll get valid inputs from that point on. But you don't get lineage or where the wrong input originated from and how it got there. Which is the part that makes debugging and refactoring harder without static types. I've always found that's the thing that I miss most, the automatic lineage tracking that just highlights in my editor where the production of the wrong types comes from.
The benefit and the costs š
@gaverhae and @didibus thanks for giving more feedback. my contention, which the abstraction itself seems to promote, is that a single item channel is just a promise, and if that's all you need, you should use it, because it's more specific, informative, and less likely to introduce issues because of an overly flexible api. I'm focusing on queues because it's integral according to the rational: > This is often achieved via the introduction of queues between the producers of data and the consumers/processors of that data.
I still need an hour to review some of the examples of core async on the front end, as i think those are more informative then arguing if it's a good idea, as ideas tend to always seem good unless it's clear what the alternative would have been. The rational argues independent systems can benefit from channels, I'm saying that internal subsystems in a browser client don't need that: they're all coupled to modifying the dom. E.g one isn't crunching numbers because the server already did that. So that only avenue left is managing channels between the client and other physical distant components aka the servers. In those cases, the order of events is already managed by a more complex structure than a queue.
I think focusing on the queue though, which is the underlying implementation mechanism, takes you down a misunderstanding of the CSP model of concurrency. I think it's hard to gage if you could benefit from CSP in the frontend if you don't understand CSP and think of it like a queue. If you do, then you are only answering if you should use queues inside your frontend code. But you want to know if you should use CSP.
When you do (chan) you get an unbuffered channel, there is no queue here. You cannot put an item in it and leave it there, because there is nowhere to leave the item.
So when a process wants to put an item on a channel, the process has to pause and wait, and continue to hold onto the item, until another process shows up and takes the item from them.
Both the producing process and consuming process have to meet at the same time in order for them to exchange a message. This is called a rendezvous.
Now, if the producing process doesn't want to wait for another process to meet up with them and take their item, you need to have a buffer. That's when you can do (chan 1), which creates a channel with a buffer of one. Now the producer doesn't need to rendezvous with the consumer, they can leave the item in the channel's buffer and go do something else, so the producer and consumer process don't have to meet at the same time, they can meet asynchronously instead, but up to a limit, if the channel buffer is full, the producer will have to wait again for a rendezvous. Or the producer can choose to drop their item (discard it), and they can choose which one to drop, the one they have now, or one from the current buffer.
So it's better to think of a channel like a "meeting place". A place where processes meet up to exchange information. That meeting place (aka channel), could optionally have a drop box (buffer), where people can leave things for others to pick up at the same meeting place.
Funny enough, "rendezvous point" is a synonym for "meeting place".
Contrast this with a promise . A promise is like a magical box, I give you this magical box, it's empty at first, and magically it will appear something in it. At the point where something appears in it, it will buzz to let you know something has appeared.
Once something appears in it, it's over. Nothing else can appear, you cannot reuse the box, and you also cannot remove the thing from the box.
Promise example:
(def p (promise))
(deliver p 42) ;;=> Current process doesn't need to wait
@p ;;=> 42
@p ;;=> 42
(deliver p 10) ;;=> Current process doesn't need to wait, deliver drops since promise already realized
@p ;;=> 42
Channel example:
(def c (chan))
(>!! c 42) ;; Current process has to wait here, this will therefore "pause" the current process, either blocks or parks depending on what the process is running on (thread or fiber). They are waiting at the "meeting place", which is `c` for another process to show up and take 42 from them.
;; Assume the below is in another process
(<!! c) ;;=> 42, our process meets with the previous process, they are now both at the meeting place `c` and the first process hands over 42 to this process
(<!! c) ;;=> Current process has to wait here, the previous process has left the meeting place `c`, and is no longer there. So there is no one else at `c` and therefore nothing to take from anyone. So our process will block or park again, waiting for another process to show up.
;; Assume we are back in the first process
(>!! c 10) ;; The first process shows up to `c` again, and there is already someone there, so they give them 10 and leave.
;; Assume we are back to our other process
(<!! c) ;;=> 10, they were already waiting here, now that someone showed up, they got 10Now let's look at something a bit more like a Promise, and also see how it differs:
(def c (chan 1)) ;; Channel with a buffer of 1
(>!! c 42) ;;=> Current process doesn't need to wait, can put 42 in the buffer and leave the meeting place `c`
(<!! c) ;;=> 42, no waiting needed, there was something in the buffer, so when process went to `c`, even though no other process was there, it grabbed what was in the drop box and left
(<!! c) ;;=> Process has to wait here again, as it revisits `c`, there is no other process there, and the buffer is empty, so it has to wait.
;; Assume in another process
(>!! c 10) ;;=> Our process goes to the rendezvous place `c`, and the drop box is empty, so it leaves 10 in it and leaves.
;;=> There was another process already waiting in `c`, and so it immediately picked up the item that was left, which was 10, so the previous process now wakes up and gets 10, the drop box is empty again
(<!! c) ;;=> The process has to wait again, as nothing is in the drop box at `c`.Whoever said the "process" is as important as the "channel" is correct here. In fact CSP stands for concurrent sequential processes (no mention of channel in the name š ) The main difference is that in CSP, you need to start having the concept of these processes. In the Promise case, the same process could deliver to itself, there doesn't even need to have other processes involved. In the case of the channel, you really needed to have 2 or more processes to even demonstrate the channel behavior. I think in the frontend, this would be the first thing. Identity what concurrent processes you have or could have. Maybe each widget is it's own process. Now you want widgets to concurrently handle user events, make remote calls, and re-render. The browser could be seen as it's own process. When user click on the browser, you want it to tell the widgets that a click happened. Put those click events on a channel. Do you want the browser to wait for a widget to handle the click event? Probably not, okay add a big buffer, decide what to do if there are so many clicks and widgets are way behind on handling them. Maybe the click event is fanned out, to all widgets, and each widget receives it, and decides if they care about it or not. If they do, they can go and handle it. Now if one widget has to wait for another widget, they can rendezvous on another channel, etc. Say you type in one widget, it should update another, they can have a channel between them, the one where you type it puts a message to the other, when the other is ready it handles it.
Pedantic nit: CSP is 'communicating' sequential processes
@didibus thanks, really, i appreciate this back and forth were having. Your example of widgets talking to each other over channels is interesting, what do you mean by widget? > Do you want the browser to wait for a widget to handle the click event? Probably not, okay add a big buffer, I feel like this is the kind of example that confuses me, because typically a browser app always reacts to the user click immediately to give them a good experience. e.g if a user clicks "buy" on an item, even if the server has to process their credit card, you still show them something right away to give them feedback. you don't treat them like a second class citizen because the browser is for them. This is maybe at the heart of what i'm confused about, the browser isn't there to process it's there to react to the user and show them something to prompt for further input from them. > Say you type in one widget, it should update another, they can have a channel between them, the one where you type it puts a message to the other, when the other is ready it handles it. Why use a channel? if you just want to display it, then you would mutate the state of the message. e.g the events startin on the left would look like: [+h +e +l +l +l -l +o] and be turned into the current state: "h" then "he", etc... the que would only be useful if you were doing to do more then just pop each one off ... right? Someone linked this video https://www.youtube.com/watch?v=AhxcGGeh5ho and in it david lists a lot of interesting examples, he says one of the reasons he wanted core async is so we could abstract away the source of the events, aka, the browser vs mobile. I'll need to think about that, intuitive it makes sense, but it keeps pushing the value protestation further away. However, it's maybe the first time i was like: "yeah, ok, i get that" because it tries to decouple the producer (servers/user) and consumer (user/servers). I think react just solved this by making all producers and consumers look the same. I feel like thats a big part of this, i feel like react over laps a bit with csp and it makes it hard to see the value proposition with whats left? A lot of his other examples are also interesting, like calling multiple web servers and taking the fastest response...however, that "feels" contrived to me. Like, why is the browser ok with any server response? Why are there multiple servers that can respond with data thats ok? Are they in different phsyical locations? Is this a speed issue? if so, why is it being solved here on the client where... its just so weird. Anyway, thanks again, ill keep poking at this.
Calling multiple web servers probably isn't something you want to do client-side, but it's a real use-case and I saw it recently in gRPC: https://grpc.io/docs/guides/request-hedging/ I agree with the 'react' assumptions, basically you want asynchronous events to appear to be processed instantaneously for the most part. Core async is about complex coordination and implies non-determinism. Maybe if you're doing a simulation or art that might be cool, but that's not the kind of application most people write.
CLJS/reframe has an event queue and used core.async initially, but they switch to a hand-rolled state machine.
you don't need all that machinery to have queues and side effects and decoupling
re: āMaybe each widget is its own processā I assume āwidgetā roughly means component, in the React sense. FWIW, I am not a fan of this general frontend approach, if you can avoid it. It becomes message passing and very OOP-like very quickly. As perf optimization for limited cases itās OK (i.e. I really need to deliver data X to component C in the quickest way possible). Generally, I much prefer the approach where the state is driven, ideally, by a single atom for the whole front end app. Itās much more consistent that way and you can reason about the state as a whole. Once you start doing asynchronous message passing from any<->any, itās effectively a mini-distributed system in your frontend. Thatās hard, for no apparent benefit, usually.
When React + core.async were new, that was one of the first approaches people experimented with. I think itās been tried, and Iāve done it personally ā it was a mess.
yeah, frontend is hard enough without being a distributed system
For frontend specifically, I think core.async really shines when it handles the edges of the system. I.e. messages coming from the network, user events - clicks, taps, scrolls, key presses. Events go through core.async, get filtered, processed, ordered, etc and end up in an atom, most typically.
The more you can keep the rendering close to (= view (f @atom-state)), the better
The computer can't simply "always react to a user". You want to make it appear that way to the user to provide a nice responsive experience, but under the hood you need a way to attempt to give that illusion. That's where concurrency comes in. I think what's a bit confusing in the browser JS environment is that there's already other models of concurrency. But I'll give a better example. Say you want to implement a browser based minimal Slack app. You have three panes, on the left, a channel selector pane, on the right a channel messages pane (showing latest messages posted to the channel) and at the bottom a message pane (to type a new message in the selected channel). The message pane has to update itself in new message being posted to the channel. You have a websocket subscribed to the selected channel and your server pushes new messages to it. Can you use a promise? It would be something like (psuedo-code):
(def ws (make-ws ...))
(def p (get-message ws)) ;; Returns a promise
(then p #(update-message-pane %))
But that just gets you the single next message someone posts. So maybe you shove it in a loop?
(def ws (make-ws ...))
(loop [p (get-message ws))]
(then p #(update-message-pane %))
(recur (get-message ws)))
Except, this doesn't work. then doesn't block and returns immediately. So recur loops right away, tries to get another message, get-message returns a promise for the next message, and then you install another then on it, and so on. There's no backoff, you're like creating promises for infinite messages to come with no end in sight, there's no waiting and so on. Also, internally get-message would need to track a queue (here it is š) of all these promises so it can deliver to them as messages actually come up in order.
Ok, so what does JS do? It uses an event subscription (pseudo-code):
(def ws (make-ws ...))
(add-event-listener ws :on-message #(update-message-pane %))
Now in a sense this is fine, but it's back to "callback hell", and also we're skipping over what websocket has to do to expose this.
Can you use CSP (pseudo-code)?
(def on-message (chan 100))
(def ws (make-ws ... on-message))
(go-loop [msg (
Pretty clean. The go-loop waits for a message on the channel, when there is one, it updates the pane with it. The websocket puts messages on the channel as they arrive. We buffer 100 messages in case messages come faster than it takes us to update the pane, now we can assume the websocket can do something if it's unable to put on the channel, tell the server to backoff, if that's not the case, then maybe we increase the buffer size or we use a sliding or dropping buffer.
So in a way, it be nice to imagine JavaScript defaulting to CSP, and js/websocket working this way. But you can also wrap the JS event in the above (pseudo-code):
(def on-message (chan 100))
(def ws (make-ws ...))
(add-event-listener ws :on-message #(>! on-message)) ;; And maybe you'd need to see here if you can handle the backoff, though I'm not sure websocket supports backoff
(go-loop [msg (that sounds exactly like the intended use of an es6 generator
I think you can probably do it with async-for over an Iterable, too: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
The nuance I saw that differentiates async/await and CSP is just that CSP can block the sender? But you can implement one in terms of the other.
Let's say you have to await after a send before you can send again, you've done it
backpressure!
What do you await after a send?
throw an exception on the second send, return a promise with the next thing you can send to
not a great interface, but it's possible
I've been doing some rust, which colors my thinking, but there you'd just move the value and have a compile-time assertion you can only send once to it.
Not sure I follow? But even if you could, that's just focusing on the consumer side, but think about the websocket internals, aka, the producer... It would still need an internal queue (or buffer), and then it would need to shuffle from that internal queue onto a series of promises and so on. So all concurrent model I think can be used to build the others I think, at least all the main ones. It's more that, you're not straight up using the promise async/await model, you're like building higher level protocols on top of it and having that mimic the behavior. CSP would just directly give you what you need in this case.
But.. you build abstractions on top of core.async, don't you?
It's not a big cost to do that for promises or whatever in a shared library.
Not normally, you wouldn't go and build new concurrency abstractions on top of it. You'd build like application specific abstractions on top of it, but not new concurrency constructs. You might if you think you're missing something. But this is kind of a dead end argument. You could code in assembly, build all the abstractions you need on top, build your own function calls, your own for loops, etc. Why use Clojure? You can just use Java to reimplement a Lisp like DSL abstraction of your own.
Like the argument worth having is what you should use as your starting point. Should you build on top of async/await, or on top of core.async.
Not exactly, async/await is already there. You have to decide to bring in core.async
That's just another bullet in the pros/cons decision you'll be taking to answer the question. It's the same question though. One downside of core.async is you'll have to bring an extra lib in.
I'm not arguing that you should choose to use core.async. I'm trying to provide intuition into why you might want too.
For me the answer is you intend to build or reuse a lot of code that benefits from what's unique in core.async. Maybe it's CLJC, maybe you're using it on the backend, too. Maybe it matches well with your business domain (eg a simulation use-case).
I am having the opposite experience. We are using an aws lib and prefer it didn't use core.async, because it uses it very lightly and nothing else in our app uses core.async.
> For me the answer is you intend to build or reuse a lot of code that benefits from what's unique in core.async Ya, but you can't decide that unless you have a good intuition of how you leverage core.async in the code you intend to build. I think that was @drewverlee question. Like, help them understand the value proposition, and then they can decide when it could make sense to use it or not.
That's fair, but that's a value judgement. You find the added deps on core.async and the bloat on bundle, slower require time, etc it brings isn't worth the light usage of it.
I can't make that call for you, I don't know the context, what matters in your case, etc.
I guess it sounds like you need to know enough about either core.async or what you intend to build before you can evaluate whether it will fit together. If you know a lot about frontend but feel something is missing in your frameworks, I doubt it's 'I really wish I had queues, backpressure, and inversion of control'. Then you scope out a small POC to learn just enough about what you don't know to make a decision.
That's a lot better than never making a decision or building for a year before you understand the ramifications of your decision
> ... you need to know enough about either core.async ...
Well, isn't that always the case?š For example, I had no idea what an "es6 generator" is until today. AFAICT, it's a bit like a (go ...) in core.async. According to Google AI summary:
ES6 (ECMAScript 2015) introduced generators to JavaScript, which are special functions that can be paused and resumed
@didibus Yea, the websockets fit much closer to core async's model imo and it compose nicely with the rest of clojures collection functions (fns) which is net win that's only improved that composed collection can be replicated between different producers and consumers e.g browser <-fns-> server <-fns-> browser. Here again, i think react ate a lot of the lunch because the frontend dev is first and foremost responsible for the view maintenance and so React Native components which can run on multiple consumers solve this problem in the headspace their accustomed to.
The issue becomes how do you smoothly transition: lets say one day you want to add a mobile view, how easy is it to transition to using core.async to accommodate that? Or maybe even just moving from http to websockets would be enough.
WebSockets seem like a better fit for core.async than HTTP requests, but in reality, they're essentially the same. Typical HTTP req/response is like a WebSockets, where the WebSocket disconnects frequently or after every request. And WebSocket disconnects happen all the time in the real world, both with regular browsers and mobile devices.
when a browser makes a websocket connection it implies it won't be explicitly asking for incoming events, which means it's far more likely those events ill pile up and require some kind of storage like a queue.
A tool can't be a good fit for every situation.
Sure, that can happen.
I think there's overfocus on the "backpressure" problem. More often than not, that's not a critical concern. I.e. modern browsers are quite powerful, can buffer a bunch of data, buffering happens at the TCP layer, etc, etc. The other benefits and potential use cases of core.async we've discussed already.
Ya, the issue is that JS has alternative ways of modeling concurrency. And in the end it'll be deciding if core.async brings something nicer over them that's worth going against the defaults. JS has async iterators since ES2018, these are not the same as Promises, though they leverage Promises as well. And similar to how Promises have async/await syntax sugar, async iterators has async function*/for...await syntax sugar. They get closer to core.async in that they also handle streams, and they manage backpressure too, since they are pull based. The consumer asks for the next() element when it's ready to receive it, and the producer returns a promise, the consumer then waits for the producer in turn to deliver to the promise. So a bit like a chan with a buffer of 0 (unbuffered chan). Unlike a channel, they don't support fanning out, being joined, applying transducers, etc. And are meant to be a one to one, one producer to one consumer. Revisiting the example with async iterator (pseudo-code):
(def ws (make-ws ...))
(for-await [msg ws]
(update-message-pane msg))
Also they have pretty good error handling and they don't use nil as the end marker which I think is nicer. They can also short-circuit.
So if you need some of the features channels have like fan out, joining, transducer application, filtering what goes to what consumer and so on, channels are still nicer as those are out of the box.
And if you need a buffer greater than 0 or sliding/dropping, channels are also better. We can see that one with this example.
If you needed to expose an async iterator from the web socket, it's more complicated to wrap it in an async iterator than it was for wrapping it in a channel.
That's because it's "buffer of 0" and pull based. I'm not even too sure how to do it, but I think something like (pseudo code):
(def ws (make-ws ...))
(async-generator []
(let [q (mutable-queue))]
(add-event-listener ws :on-message #(push q %))
(while (not (empty? q))
(yield (pop q)))))
Since websockets are pushed based, you need to hook up a queue to buffer things and then on-demand pull from the queue as the consumer requests the next item. Channels are hybrid push/pull, you can do either, so they work more naturally in this case.Hum, actually I don't think that pseudo-code works, it's not supposed to exit the loop when the queue is empty, it should wait on the queue having elements again somehow. Anyways, I'm sure it's possible to adapt, just might be a bit tricky and more convoluted.
I think the issues with core.async especially in CLJS, like @gtrak said, the added dependency, the fact the rest of the ecosystem doesn't use it, etc. Then I'd also say, core.async itself has a few annoying quirks, nil as the "end" sentinel, so you can't pass nil as a message. And maybe worse of all, the error handling haha. It's not that hard, but if you want to short-circuit from the consumer, or propagate an error down/up, and so on, you have to be more careful and think explicitly about how you'll do it. But still, for use-cases more complex then, go do one thing and let me know the result when done (for which promise async/await is quite nice for), I'd say core.async is still pretty neat. Like if you keep thinking about my slack use-case. Now you'd also want the channel selector to notify the message pane on channel change, so that it ends the websocket and creates a new one for the new channel and listen to that, as well as update itself to reflect the change. So you can push a channel-change event on the same channel that on-message are being pushed as well and for which the message pane is handling events coming in. And when you close the socket to the old channel and create a new one, it can reattach to that same channel as well. So you see how the decoupled channel which allows many to many starts to come in handy as well.
I would like a bit more rationale of why nil core.async handling is not good apart from āItās annoying, I donāt like itā. In reality, IMO, itās actually a good thing. Thereās very few use cases, perhaps zero, where Iāve thought āI really need to put nil on the channelā. And if you definitely must do it, [nil] is always an option.
nil / NULL is the billion dollar mistake that weāre all sorta living with š The more we push nil towards one use-case where it definitely means something, the better.
Otherwise, nil can literally be anything; itās the most overloaded concept, ever. And thatās most typically not a good thing.
If you make the choice to āprogram in CSPā, specifically core.async flavor of CSP, the nil mistake gets āfixedā, a bit.
Well, I have wanted to pass in nil sometimes, and it's a bit annoying not to be able to do so. But the more annoying part is that if you put nil on a channel it throws. So you have to constantly be careful and guard that you're not about to put a nil.
So in my example above, what if you get nil from the websocket, and now you thought you would just do on-mesage put to channel, but now one day it errors, and you realize, oh damn, have to add a nil check, and now I feel I'm in Java again with if (not-nil?) everywhere
How do you get nil from a websocket? Are we assuming itās receiving transit data or such?
Hereās a somewhat more formal take on that topic: https://aphyr.com/posts/320-returning-self-or-void-suggests-mutability
Well, not sure if on websocket you an get a nil, but say you parse something out of the socket and then put the body of it on the channel, now maybe that's null.
Nobody ever feels good about nil ⦠But I think that article articulates why itās not good pretty well. Basically, it comes down the fact that if you do believe that functional programming is a good thing, in most cases, then there could only be one function that returns nil ⦠If you literally anything can return nil , itās not actually⦠functional programming.
Not sure channels and CSP is "FP" to be honest. Plenty of mutation with channels
Internally, yes. Channels are stateful, yes.
But arguably, channels with transducers are ⦠a very nice mix.
Itās like the wormhole that connects the two worlds š
The nil argument goes beyond CSP, itās actually a good practice, IMO, for Clojure and any language in general that has to deal with nil .
Very typical clojure code is (defn f [x] (when (check123? x) (do "something")) That is effectively (provably?) not a pure function.
Actually functional: (defn f [x] (if (check123? x) (do "something") :i-could-not-do-something-with-x)
Ya, but it's the mismatch. If Clojure didn't have nil, and core.async didn't, then ok. But Clojure does have nil, interrop has nil, and now when you hit core.async it's like, euh... what do I do. And you need extra handling.
(defn f [x] (if (check123? x) (do "something") :i-could-not-do-something-with-x) this functional returns, for any input x , either "something" or :i-could-not-do-something-with-x (assuming check123? is also pure)
I think Clojure wanted to be very interop-friendly, so nil was definitely staying.
You can definitely create a brand new world, without nil but AFAICT the interop suffers, a lot. Did Kotlin did that? Not sure how they handled it.
Hum... also not sure I really agree with your example, I mean, both are impure function, because do something is probably a side-effect. Returning "nil" to mean I didn't do anything, or we found nothing, no result, etc. It's fine, it does not give a clear reason, and I've seen people say like better to return :nil/not-found, :nil/not-allowed, :nil/skipped, but at the same time, the caller often doesn't care about the distinction. So now you force them to handle all kind of nuances of "nil" they don't care about.
Kotlin has nil, they just have types that don't allow nil as a value in the type system.
Well⦠the caller doesnāt care⦠until they need to do (+ i-did-not-care 42)
(in JVM clojure⦠in JS ⦠oh my⦠even worse lol)
I mean, there nil is 0 which is OK⦠perhaps by a JavaScript historical accident haha
Thatās the thing⦠if the code is actually important, and thereās a chance for ambiguity, you typically cannot afford not to careā¦
No, I mean more like:
(-> (a) (b) (c))
Nil punning mean that, all three function, a,b and c will just return nil if given nil. So now if (a) cannot return a result for wtv reason, it returns nil instead, the whole thread just short-circuits itself because all 3 functions properly handle nil.
But if (a) returned :that-didnt-work, and then (b) has to be aware that it's a that is piped into it, or the thread has to get more complicated and you insert like "error handling" in-between to handle the :that-didnt-work. And then if (b) returns :nothing-found, now it's like each method has these custom "error" returns.
> both are impure function
Ok, more specific (defn add-42 [x] ) (if (number? x) (+ x 42) :nil/invalid-number) ) . Is that not a pure fn? For all numbers it adds, for everything else it returns the keyword.
That's a better example š
Well, yeah⦠typical Clojure-style some-> threading gets sorta messed up with this approach.
But I personally donāt use that often⦠Itās often too hard in practice to ensure everything works smoothly with nil . And yes, thereās fnil ā¦
One can perhaps come with a custom some-> that works with the :nil/my-types ; But that would be a project convention, itās not used widely.
Ya. Like I always liked that idea, but there would need to almost be a polymorphic nil. Like if you could actually do nil/foo, nil/bar and both would be (nil?) true. So you could treat them all like nil, or if you wanted, handle each specifically.
> polymorphic nil Yeah!
But alas, I think interop prevents that.
All bets are off with interop
Always
I mean, one can definitely wrap those interop calls to make them āobeyā a custom convention, but again: manually.
original source of some->
(defmacro some->
"When expr is not nil, threads it into the first form (via ->),
and when that result is not nil, through the next etc"
{:added "1.5"}
[expr & forms]
(let [g (gensym)
steps (map (fn [step] `(if (nil? ~g) nil (-> ~g ~step)))
forms)]
`(let [~g ~expr
~@(interleave (repeat g) (butlast steps))]
~(if (empty? steps)
g
(last steps)))))See that hardcoded nil? ā¦
But I'd say nil in Clojure is way less a problem, because it's a value. In Java for example, null is a pointer pointing to nothing. So then you have like a.b and a is a pointer to nothing, so when it looks up b it can't find it and errors.
But in Clojure, since it's FP, you'd do (b a), and b is always going to be found (well caveat if you like redef vars dynamically), but it's kind of like a static reference to a function. So now if a is nil, it's not a pointer dereference error, it's a nil value passed to b and b can treat it like any other "bad input", or make it a "valid" input if it wants.
Well, in general you don't even need some-> because most Clojure functions if given nil return nil, and so -> just works. Which is nice.
So it's really kind of like nil and :nil it's the same. So I think we need to discuss more what's bad about:
(defn add-42 [x] ) (if (number? x) (+ x 42) :nil) )
vs
(defn add-42 [x] ) (if (number? x) (+ x 42) nil) )
Or maybe both are bad š
right⦠I think thatās the clearest argument https://aphyr.com/posts/320-returning-self-or-void-suggests-mutability
Have you ever ran into the problem that you get a:
Cannot invoke "Object.getClass()" because "x" is null
⦠and on first glance you have no idea where it came from?(thatās the output of (+ 1 nil) in JVM clojure)
Because, yes, most of Clojure handles nil well⦠until some interop happens, which is what arguably + is doing (almost)
Ok, read the article... But I think the article is talking about functions that return only nil. Maybe I misread. But I think it means like, if a function always return nil... like what could it be doing? It's probably side-effect and just a way to have void return in Clojure.
Haha, ya I agree. But, that's now the issue of nil in the interrop language. If say Clojure was not hosted, but it's own language, I think nil really would not be that bad. Because the way Clojure handles nil is a lot better. It breaks down in that we still encounter null pointers when doing interop, so we see some of the ugly side.
> If say Clojure was not hosted, but itās own language, I think nil really would not be that bad.
Probably correct, yes.
Like, let's say: (get {:a 1} :b)
So get will return nil. Alternatively it could throw, or return say :not-found. Honestly, I feel all three have pros/cons. :not-found is explicit about like why we don't have a result, and it's not confusing with say (get {:a nil} :a) which would also return nil but not because it was not found. But :not-found needs to be custom handled by the caller, and there's no standard, how do you even know it could return :not-found, every function could choose to return a different keyword, and so it's hard to handle I feel. At least nil is consistently handled.
Anyways haha. Back to core.async. I feel maybe there' two things. Why can't you pass a nil on the channel. And say ok that's maybe a good thing, pass a better message like :not-found. I think it's still bad that nil is the closed indicator. Why not return like :clojure.core.async/channel-closed or something like that. And then provide a closed? predicate. Now it's closed = nil and you check for it with nil? , it's weird.
Hum, but I'm willing to take that criticism back haha. You made good points. Also, I reread the doc-string of close!, and you can keep taking on a closed channel until you've emptied it's buffer, at which point it returns nil. So nil? doesn't mean closed. It's like closed + drained, So it makes more sense, you take from the chan, and the chan says, there's nothing left to take here.
Or alternatively, what if⦠Instead of returning this custom :nil/custom-cases ⦠we literally return the expression that returned nilā¦
a key distinction here is concurrency vs. parallelism. outside various optional worker stuff, js doesn't have parallelism, but because it has at least two sources of events (the server and the user) it does have to manage concurrency
> timeouts are a third source of events now that I think of it Thatās⦠an astute observation.
And if youāre calling 3rd party services (analytics, et al), each one of those services is a separate, independent source of concurrent events.
This an old video by David Nolen about core.async. I havenāt re-watched it recently, but I do recall it as one of my first āahaā moments about core.async https://www.youtube.com/watch?v=AhxcGGeh5ho
From there, I think, I learned about the possibility of āputting channel on a channel,ā which is a very useful technique for certain cases.
(defn add-to-second-position [lst item]
(let [[start end] (split-at 1 (if (list? lst) lst [lst]))]
(concat start [item] end)))
(defmacro lisp-some->
[expr & forms]
(let [g (gensym)
steps (map
(fn [step]
`(if (nil? ~g)
nil
(let [ret# (-> ~g ~step)]
(if (nil? ret#)
[nil (add-to-second-position '~step ~g)]
ret#))))
forms)]
`(let [~g ~expr
~@(interleave (repeat g) (butlast steps))]
~(if (empty? steps)
g
(last steps)))))
Edit, warn: broken, do not use.(lisp-some->
{:a {:b {:c 42}}}
(get :a)
(get :b)
(get :c))
;=> 42
(lisp-some->
{:a {:b {:c 42}}}
(get :a)
(get :b)
(get :cc))
=> [nil (get {:c 42} :cc)](lisp-some->
{:a {:b {:c 42}}}
:a
:b
:cc)
=> [nil (:cc {:c 42})]Returns ātaggedā nil, where the second part is the code that triggered the nil !
I just came up with this idea on the spot, so any opinions are welcome, hah.
Ya, the more we talk about this, the more I feel maybe nil is a poor man's error return. If we actually returned error values, and had good pattern matching on them and also maybe could recognize them as a generic error?, and then we banned nil, and always returned errors. That might be all around better
> nil is a poor manās error return
Yeah⦠or basically papering over a ton of cases, errors, mistakes, missing data, etc. Basically the overload of all overloads, so to speak, hah.
> And what you are doing is a bit like shoving a āstack traceā Yeah I donāt know what I just did but would love to know how itās terrible because in this second I like it too much.
Which is typically a bad thing LOL
On the surface, itās not pure-pure, but itās way better than only nil
As in, there could be many functions that return [nil :a] for many reasons, so it fails in the theoretical purity sense. But in practice, it might be āgood enoughā
Say it was like:
(defrecord Error [type trace message parent-error])
Or give any other name, maybe we call it Nil haha
Say you could then make it work with some-> and nil? and even (if x ,,,) and if x is an "Error" it results in falsy
A full example perhaps?
Are you trying to create a custom Error type?
What I wanted to avoid with that macro I wrote above is the custom error ātypesā of :nil/blah :nil/foo
I quickly realized that once you have all those custom cases, now you need to change all fns to be sorta aware in their return statement to return that, which is annoying, tedious, manual. Not even getting to interopā¦
With the lisp-some-> you can happily continue returning nil and any existing fn that returns nil works⦠but lisp-some-> simply bails on the first nil return, returning not just nil but also the code that triggered itā¦
Ah yes I see. You mean you no longer have to say :not-found it's just [nil <code-that-caused-nil>]
Yeah! I first wrote a version that was doing that⦠And realized it was not really greatā¦
Ya, I also just realized another use of nil that would be harder to replace. How do you make "optional" arguments.
as in, was checking :nil/problem ⦠was checking the namespace of that kwd is "nil"
Thatās like⦠which kind of optional?
The new-ish Clojure syntax or?
& {:keys [...]}
Honestly either. Say:
(defn foo [a b]
...)
;; b is optional, so can be nil
Or like
(defn foo [a & {:keys [b]}]
...)
Same thing here, if you call with just an a, inside foo it knows by checking if b is nil or not.I see, ok. Yes, nil is quite embedded in Clojure
In newer code, I wouldnāt personally do (defn foo [a b]) where b is typically expected to nil
If Iām leaning towards flexibility, taking two maps tends to work well, I think, like:
(defn fn42 [{:keys [always here]} & {:keys [maybe-not-here]}])
āfuture proofā, of sorts
Ya agreed, but just saying, it's hard to imagine what else Clojure would do in that scenario (of supporting optional args).
I guess it could set the value of b to like :optional/not-present or something.
> Ya agreed, but just saying, itās hard to imagine what else Clojure would do in that scenario (of supporting optional args).
> I guess it could set the value of b to like :optional/not-present or something.
I think the optional stuff works pretty well, and itās not much of an issue. Like you were saying, that is essentially āpure Clojureā so nil works quite well there.
The only sketchy part is if you use :or and you donāt want to have nil as the :maybe-not-here but, unfortunately, the input map is {:maybe-not-here nil}
The one escape hatch I use is to do:
(defn fn42
[{:keys [always here]} & {:keys [maybe-not-here]}]
(let [maybe-not-here (or maybe-not-here "no nil for sure")]))(or ...) rather than the destructure :or
Itās my personal opinion that {:maybe-not-here nil} is bad in the first place, so I try to avoid it. But some people have disagreed with me.
Hum... right. That's true, so again sometimes nil can be a bit ambiguous, but also it's often damn convenient, short, quick.
Yeah I am not saying I care all the time, hah; For quick throw away code I use (defn f [x] (when (number?) (+ x 42))) all the time
I am talking about the cases where it matters, actually
> ānil can be a bit ambiguousā A bit, you mean āveryā, perhaps š
I'm thinking also (a/poll!) , it returns nil if no value. If channel allowed nil, it also would need to return a different indicator.
Yes
I think core.async works quite well with Clojure, it was obviously designed with all Clojure semantics in mind
Thatās why when I hear āgee, I canāt put nil on a channel, how come?ā I sorta roll my eyes, hah.
Ya, well, I've had the "can't put nil on channel" error a few times, so it soured me. And I think I had times where like I "queue up 10 request" than expect their result back in order, but I know some can return nil, and it's ok, I would handle it as like ok no result for this one. So I remember that too it felt annoying, and adding a custom (if (nil? res) (>! c [nil]) res) is a bit annoying when you feel you should be able to just do (>! c res)
Getting late for me, I'm off. GN
ttyl, good chat
polymorphic nil, careful, you're starting to sound like you want Option<T>
Ya, the more we talk about this, the more I feel maybe nil is a poor man's error return. If we actually returned error values, and had good pattern matching on them and also maybe could recognize them as a generic error?, and then we banned nil, and always returned errors. That might be all around betterthis exists https://www.shakacode.com/blog/thiserror-anyhow-or-how-i-handle-errors-in-rust-apps/ , it's quite good imoIt's nice to have the ability to deep pattern-match on a tree of wrapped error types, I needed to do that recently.
Eg the http client error has a json variant with another lib's error type in it, that one has specific variants
If I want to do something different for each error variant, I can: https://docs.rs/ureq/latest/ureq/enum.Error.html
And if you just want to convey a dynamic error, you can do that, too Other(Box, but I prefer to minimize that.
I also did some ocaml a while back, and the library we used had a different approach to async errors, which I think it stole from erlang? https://ocaml.janestreet.com/ocaml-core/v0.13/doc/async_kernel/Async_kernel/Monitor/index.html
Their approach is similar to JS promises, just better
They have something analogous to a channel. I thought it was a little clunky compared to core.async, but it got the job done the few times I needed it: https://ocaml.janestreet.com/ocaml-core/v0.13/doc/async_kernel/Async_kernel__/Pipe/index.html
I wouldn't have structured a frontend app around this, but it was useful for streaming incrementally-generated data into postgres really fast
Yeah I agree, polymorphic nil is initially enticing but in reality it's quite clunky
> this exists https://www.shakacode.com/blog/thiserror-anyhow-or-how-i-handle-errors-in-rust-apps/ , it's quite good imo (edited) Ok, I know very little about Rust, but I don't quite get the unrecoverable part. Does it terminate the thread? Or the whole program?
Welp, even on the Rust forum there appears to be disagreement about this https://www.reddit.com/r/rust/comments/161wm0p/gracefully_failing_after_unrecoverable_errors/
> "Recoverable and unrecoverable is IMO a false dichotomy."
That's my intuition also, but happy to learn more.
Ah, so apparently, if it's not the main thread, it only stops that thread, that makes more sense now.