Fork me on GitHub
#membrane
<
2023-02-08
>
Ben Sless18:02:05

I just had a weird idea - membrane's effect handling model can be used to write any sort of event driven program, where the state is just that of an event. why shouldn't I be able to run backend programs with it? I can write pure code and specify handlers when I start it up. Is this most of the way solution to dynamic algebraic effects in Clojure?

phronmophobic18:02:55

I wish it were!

phronmophobic18:02:04

I was thinking along those lines after reading and learning a bit about algebraic effects https://clojurians.slack.com/archives/C03RZGPG3/p1672777264296859?thread_ts=1672700787.847939&amp;cid=C03RZGPG3

🔥 2
phronmophobic18:02:38

At best, membrane's effect handling is an accidental algebraic effect system. It's on my todo list to learn more about algebraic effects. My bet is that there's lots of good ideas to pilfer.

Ben Sless19:02:04

I though some time ago of a model similar to membrane's intents, where side effects are represented inn data form, you call a universal handler which passes it off to the proper executor and side effect, but how does the result return? You have to specify who's called when you complete with the previous state and side effect result You either do it as a continuation or a membrane like on handler

phronmophobic19:02:34

I think fulcro does a good job of expressing that these intents/effects actually are mini-programs and the effect handler is a mini-interpreter. There's issues where you start bing able to represent intents that have programming constructs like conditional-branching.

Ben Sless19:02:55

That can be solved in the continuation, no? You specify what function gets called next, it does the branching, then produces another intent after a fork, no? My inspiration was actually how servers work, specifically ngnix 🙂

phronmophobic19:02:38

There's still some open questions that I don't have a good answer for: • can effects rebind other effects before calling them • should we just be using something like vars and namespaces? • I like the idea of saying: "run this UI with this refied set of effects." There's a longer list, but that's what I remember from the top of my head.

upvote 2
phronmophobic19:02:19

> You specify what function gets called next, it does the branching Membrane's effects does a minimal version of this where every effect has an implicit dispatch! argument.

phronmophobic19:02:03

Is branching handled by the "effects system" or does it somehow reuse the execution and eval provided by clojure?

phronmophobic19:02:29

One interesting idea views can be almost completely datafied, except for the event/effect handlers. If it were possible to have those specified with instructions similar to llvm, then you compile whole user interfaces to any target language.

🔥 2
Ben Sless19:02:50

Let's say every sequence of intents has to specify a continuation that takes all the results and the previous state That continuation is just a function, and it could do the logic, branch, then call the handler with the right intent

Ben Sless19:02:06

sort of like

[[:get-from-db arg1 arg2] next-f]
Then f roughtly
(if (failed? result) (emit [[:get-from-db ,,]]) (emit [[:do-more]])

Ben Sless19:02:27

even stuff like retries can be seen that way

Ben Sless19:02:15

And in theory this can have incredible performance, it never blocks

phronmophobic19:02:27

other use cases to consider are "composite events" like double-click or click-and-release-inside

phronmophobic19:02:49

which I think can be incorporated

phronmophobic19:02:18

what's the difference between emit and eval? I'm not saying there isn't* a difference, but I think it's useful to articulate

Ben Sless19:02:06

emit means "goto the ur handler"

Ben Sless19:02:46

That lets you specify both how to run the intent, and for extra credit, which thread pool to run it on

Ben Sless19:02:03

letting you maximize thread pool utilization. you only need three pools, in theory

phronmophobic19:02:17

right, but doesn't (eval (do-more :my :args))` also mean run the do-more handler?

phronmophobic20:02:36

there's also some features here that overlap with dependency injection libs like mount, integrant, clip, system, etc

Ben Sless20:02:01

Yes, I thought this up when I was sick to the teeth from mount

😆 2
Ben Sless20:02:13

Instead of injecting the dependencies, just say what you want to happen and pass it to an executor

👍 2
dgr22:02:54

@UK0810AQ2, I used a similar model when writing an automated trading application. The brokerage library (Java) would inject events. Those would be represented in pure data and would be passed to a handler. Each handler would return a set of intents as data (e.g., place a trade, or start getting a quote stream for a specific ticker symbol), and an effect system would interpret those intents as effects (e.g. make calls to the brokerage library). The “active handler” could also change over time, giving a state machine model. One of the intents you could return was “switch to this new active handler” (state change). The nice thing about it is that it gives you a very testable core of logic.

👍 2
Ben Sless06:02:16

Was it using LMAX Disruptor?

dgr17:02:04

No, but I looked into that. The required speed was low. I ended up using core.async, though.

elliot22:02:32

I suspect some of the complexity comes from needing to pass control flow back and forth between the effect handling system and the regular interpreter, and basically making the caller manage the difference between (eval (f ...)) vs. (eval-effect-async [:f ...]) vs. (eval-effect-await [:f ...]). To some degree if we’re making the caller manage this we may as well use standardish core.async or a similar async-like API. I’m not sure but the main options I've seen are "make people write explicitly sync vs. async code" or "make everyone write code that's actually running in an async runtime (e.g. erlang, Haskell), that gives them the uniform interface.". Perhaps there's more options in the design space though.

Ben Sless10:02:25

I guess the main difference is if you write "linear" code or provide a callback, but one can be turned into the other using a rewriting macro. The important part for me is in not specifying how the effect is run, but passing it to a handler, where the waiting code will know how to handle a synchronous or asynchronous result

Ben Sless10:02:44

This also looks suspiciously like a free monad