core-async

maybenot 2024-10-30T07:29:44.700049Z

And somewhat of a design question. (which mainly comes from my struggling with understanding how to use core async properly without stepping on different footguns I read here and there.)

maybenot 2024-10-30T07:30:15.155779Z

Let’s say I want split a program into loosely coupled modules. First approach could be to use something like mount, and share a state (atom) and a few control functions, which is very similar to standard DI approach in OO languages. Second approach is to use channels and exchange data using them. What are pros and cons of those approaches? Channels are conceptionally very interesting and allow things like pubsub, mix, merge etc, and at first I thought that it would be possible to build things in erlang-like way with communicating processes. But, unlike erlang, I feel it raises complexity a lot, I need to think about io (is ws/send is io? Where http kit calls its callback? etc), put! vs >!!c, go blocks, their limitations, transducers, pipelines, buffering strategies, etc etc and probably it’ll be easier, say, not to build pubsub, but just iteratively loop over active sockets and call it a day, etc. especially given Clojure thread safety guarantees. Would like to hear your thoughts on the topic. I really like the idea of CSP and I really like Clojure but can’t quite join them conceptually without complexity overflow. (Just to add some context, async models I find simple: kotlin async/await (but not swift one), goroutines, erlang CSP, I feel you could just use them and not fall into thinking trap)

2024-10-30T11:46:04.827719Z

it's impossible to give a great answer without the context of the actual problem.

2024-10-30T11:47:33.644799Z

e.g. I'm not really tracking why you'd need an atom for option 1, it's not clear what you're wanting to get out of async, etc

2024-10-30T11:51:02.721149Z

It seems like you might be asking, "how do you initialize a core.async program?" but the options you lay out make it seem like that's not the question(?)

maybenot 2024-10-30T12:08:05.420279Z

Yeah, sorry, I understand it may sound like a blurry description, to make it more concrete, let’s say I have a websocket service, with multiple real time connections, and flow of messages going between subset of those connections, like in slack or discord, and the question is how to structure it with core async In kotlin it could be a set of coroutines, connected by shared flows In elixir/phoenix it could be a set of genservers connected by pubsub I imagine in clojure it could be a list of conns inside an atom or alternatively it could be a core async program, but not sure how to structure it this way or does it make sense at all

2024-10-30T12:20:10.742539Z

is the set of connections dynamic (e.g. 1 per current user)? are you wanting synchronous communication?

maybenot 2024-10-30T12:31:26.103209Z

it could be several per user, e.g. desktop / mobile And both sync / and async are possible

2024-10-30T12:35:36.424149Z

gotcha. well having thought about it for like 1.5min, my instinct would be to put the connections in some kind of shared state and look them up when you need to use them (i.e. option 1, I think).

2024-10-30T12:36:43.140699Z

synchronous req/response over core.async is doable, but it's kind of a pain, esp considering the underlying connection will be synchronous anyways

2024-10-30T12:37:34.666199Z

unless the connection is async, in which case... yeah idk I'd have to play with it more than likely

2024-10-30T12:40:50.123079Z

(curious to see what others think)

maybenot 2024-10-30T13:44:40.838379Z

Thank you, makes sense, yes Seems like core async differs a lot from go or erlang where it’s kinda natural to structure the app around CSP style processes (not sure how to put it better)

2024-10-30T13:46:30.415629Z

do you even have a choice in erlang? I'd be surprised if it's any more un-natural than go. we just have different tools in addition to go-blocks.

2024-10-30T13:47:55.298699Z

but if you have a go solution, I would expect the core.async solution to more or less match go-block for go-block.

maybenot 2024-10-30T14:37:02.179339Z

> choice in erlang You don’t:) but it’s kinda good model imo > go-block Sure, I tried (and failed) to cover this in op post, it will more or less match, but I expect less mental go-tchas in go

maybenot 2024-10-30T14:41:56.102549Z

So I guess my struggle really boils down to this: core async is interesting but complex, and I wanted someone to tell me that I’m just using it wrong and it’s nice and simple haha

maybenot 2024-10-30T14:52:25.306819Z

My hope is vthreads move core.async more to go in this regard

2024-10-30T15:12:47.433499Z

> I expect less mental go-tchas in go Yes and no. core.async offers some things go channels don't (priority selects, buffering strategies, xforms). The biggest downside relative to go is the fact that you have to manually put heavy CPU usage and I/O on a thread. That said, I've never actually locked up the core.async machinery, and in general it works okay if you miss a spot, so you shouldn't be too terribly worried about it. I'm curious whether you find it more complex than go, because most of the semantics are identical.

2024-10-30T15:14:22.947509Z

(vthreads will eventually alleviate having to manually put things on thread, but IMO that's more a quality of life change rather than a massive capability upgrade/simplifier)

maybenot 2024-10-30T15:49:43.811039Z

Maybe you’re right and it’s simpler than I’m imagining, will to try to do it

1
2024-10-30T17:08:38.275259Z

Erlang doesn't use the CSP model, it uses the Actor model.

maybenot 2024-10-30T17:09:19.086649Z

Sure, that’s my simplification of things here

maybenot 2024-10-30T17:12:44.652969Z

What I mean is that it’s conceptually easy to split a program into multiple concurrent processes (e.g. representing a single connected client) and organize communication between them

2024-10-30T17:24:36.787009Z

The difference I think, is the "Actor" also contains the code. Whereas a "Process" is more like a thread.

2024-10-30T17:27:02.480909Z

So basically, I think the answer to your question is actually you'd do both #1 and #2

2024-10-30T17:28:04.530009Z

Like your modules are unrelated to core.async. You can have a namespace with functions that perform what you want done. Then you can start a go process that uses it.

2024-10-30T17:48:04.868609Z

Which I admit, makes it more confusing, because it's more open ended how to organize things.

2024-10-30T17:57:11.996529Z

For example, one approach (using Component) is something like:

(defrecord MyProcess [config state-atom in-chan out-chan]
  component/Lifecycle

  (start [this]
    (let [running? (atom true)
          state-atom (or state-atom (atom {:count 0})) ; Initial state
          in-chan (or in-chan (chan 100))
          out-chan (or out-chan (chan 100))
          
          ;; Main process loop
          process-loop
          (go-loop []
            (when @running?
              (when-let [msg (<! in-chan)]
                (try
                  ;; Process the message and update state
                  (let [new-state (process-message @state-atom msg)]
                    (reset! state-atom new-state)
                    ;; Optionally send result to out-channel
                    (>! out-chan {:status :processed 
                                  :result new-state}))
                  (catch Exception e
                    (>! out-chan {:status :error 
                                  :error (.getMessage e)})))
                (recur))))]
      
      ;; Return the component with its running state
      (assoc this
             :state-atom state-atom
             :in-chan in-chan
             :out-chan out-chan
             :running? running?
             :process-loop process-loop)))

  (stop [this]
    ;; Clean up resources
    (when-let [running? (:running? this)]
      (reset! running? false))
    
    (when-let [in-chan (:in-chan this)]
      (close! in-chan))
    
    (when-let [out-chan (:out-chan this)]
      (close! out-chan))))

;; Helper function to process messages
(defn process-message [state msg]
  (case (:type msg)
    :increment (update state :count inc)
    :decrement (update state :count dec)
    :reset {:count 0}
    state))

;; Constructor function
(defn new-process [config]
  (map->Process {:config config}))

;; Example system using the process component
(defn example-system [config]
  (component/system-map
    :process (new-process config)))

🙌 1
2024-10-30T17:58:13.195279Z

You can make process-message a multi-method if you prefer, or tweak anything. But this should show you one way to organize states/modularize using core.async

maybenot 2024-10-30T18:09:30.236909Z

Thank you! This sample really helps a lot

maybenot 2024-10-30T18:10:39.642549Z

would definitely read a book on core async, so much things there (which is good and bad at the same time)

2024-10-30T18:58:17.192709Z

Ya, I feel in general Clojure lacks articles, books, or others about the larger structuring of apps. Like all those "enterprise patterns" style things. Like, showing how to implement a chat room like app, in an enterprise way, aka, not a scrappy thing, but like proper code structure and organization, state management, handling errors, retries, logging, and all that

2024-10-30T19:00:13.408229Z

Probably because it's a small community. I think most articles/books are more in-the-small, walk you through functional programming, lisps, macros, the core functions, abstractions, and so on. But not many then show you how to leverage all that for more complex applications.

💯 1
maybenot 2024-10-30T19:09:27.401049Z

Agree 100% I found Clojure Applied the most useful on the topic, but also tbh (re)reading it brought me here:) bc I got the idea how I could bind modules with core async, but wasn’t sure why I needed it and how could I use it for something like you showed above

maybenot 2024-11-02T12:13:35.418609Z

Well, it turns out I was completely wrong and core async is awesome, and what I thought was a source of complexity turned out to be a great transparency tool to understand what’s going on under the hood (as opposed to magical runtime) (and that was quite predictable tbh..) clj Thanks for advices!

2