http://logic.cs.tsukuba.ac.jp/~sat/pdf/tfp2020.pdf
In short: algebraic effects.
Here’s a whole thesis on the cool things that can do done with this one simple trick: https://publikationen.uni-tuebingen.de/xmlui/bitstream/handl...
"Professors HATE HIM"
I gave a talk about it recently at Michigan Typescript: https://www.youtube.com/watch?si=Mok0J8Wp0Z-ahFrN&v=uRbqLGj_...
https://github.com/thefrontside/effection
https://github.com/neurosnap/starfx
With delimited continuations, we are able to express any async flow control, it is an incredibly powerful paradigm.
It's just the way we force the results to appear that differs.
IO Monad, if you squint, is extremely similar.
Continuations are a different way to "store a procedure and state" (like a partially evaluated function in a lazy sense).
It's not totally obvious, but it's a lot of fun to think about how these things are all related.
In languages such as C++ and Java, raising an exception defaults to an unwinding only if not handled within the same function that raised it.
Then there are languages with resumable exceptions, and languages wherein unwinding is considered normal control flow as "shortcut returns".
Exceptions are merely the record of the programmer's mistake. Essentially an error, except an error that could have been caught at compile time given a sufficiently advanced compiler.
Exception handlers provide a control flow for dealing with exceptions, which may include unwinding.
Exception handlers, while primarily intended for use by exceptions, are not necessarily restricted to exceptions. Often programmers use them to move other things around, most notably errors.
From an interface-contract point of view, exceptions model the case that an operation cannot complete normally. Any mechanism that enables an operation to complete normally after all upon encountering an error condition, should better be modeled with a separate language feature, for example with callbacks specific to the concrete error condition.
Instead, define effects that are actually relevant to your particular app. DB reads/writes, I/O to third party systems.
Model those as data-driven effects. Keep the rest pure.
state1a state1b state1c | state2a state2b state2c | state3a state3b state3c
This means wait for state1a, state1b state1c in any order, then move to the next sequence of things to wait for.In a multithreaded server or multimachine distributed system, there are global states you want to wait for and then trigger behaviour. The communication can be inferred and optimised and scheduled.
It's BNF syntax - inspired by parsing technology for parsing sequences of tokens but tokens represent events.
If you use printf debugging a lot, you know the progression of what you see is what happened and that helps you understand what went wrong. So why not write or generate the log of sequence of actions you want directly not worry about details?
But wait! There's more. You can define movements between things.
So take an async/await thread pool, this syntax defines an async/await thread pool:
next_free_thread(thread:2);
task(task:A) thread(thread:1) assignment(task:A, thread:1) = running_on(task:A, thread:1) | paused(task:A, thread:1);
running_on(task:A, thread:1)
thread(thread:1)
assignment(task:A, thread:1)
thread_free(thread:next_free_thread) = fork(task:A, task:B)
| send_task_to_thread(task:B, thread:next_free_thread)
| running_on(task:B, thread:2)
paused(task:A, thread:1)
running_on(task:A, thread:1)
assignment(task:B, thread:2)
| { yield(task:B, returnvalue) | paused(task:B, thread:2) }
{ await(task:A, task:B, returnvalue) | paused(task:A, thread:1) }
| send_returnvalue(task:B, task:A, returnvalue);
Why not just write what you want to happen and then the computer works out how to schedule it and parallelize it?I think iteration/looping and state persistence and closures are all related.
I have a parser for this syntax and a multithreaded barrier runtime which I'm working on, I use liburing. I want to get to 500 million requests per second of the and ~50ish nanosecond latency of LMAX Disruptor.
The notation could be used for business programming and low level server programming I think.
IMHO petri nets are the most widely applicable method for modelling concurrent processes that I've seen yet.
Would you like to talk more about this subject?
https://ubf.github.io/ubf/ubf-user-guide.en.html
but part (c) was only ever a sketch of a service/event grammar.
I implemented REGEV (regular expression for events) in my previous job. It was a REGEX for event types, but in that case, applied to log/trace emissions from test runs. So it allowed you to specify a regex of what events to expect for success, and had various listening and pattern-matching streams for runtime verification. It did not generate full-blown state machines for every protocol grammar. The wildcard matching helped ensure the tests were not fragile to minor changes to the code, or extra trace/log emissions.
So I think the opposite (complement) of what you were describing - not a specification of what should happen, but a grammar pattern to match downstream of what actually happened.
P.S. It's closed source, but the company is bankrupt, so maybe the IP will surface some day.
Did you regex engine support commutative events? (Happen in either order?)
My state machines in those contexts are such beasts sometimes, when you have to account for different combinations of DMA progress/completion, etc. Sometimes the hardware limitations you need to handle in software makes you really bang your head to the wall...
If you have some taskgraph that is static or predictible (think closed-loop control) and you need low latency this might be your best option.
Conway worked out some results for non-serialised (commutative) events in Regular Algebra and Finite Machines about half a century ago.
It's literally as simple as it sounds:
https://gobyexample.com/goroutines
Want to wait for a bunch of parallel coroutines to finish (sync) before proceeding? Sure, that's easy too:
https://gobyexample.com/waitgroups
This simple primitive is one of two major go features that made me not really pursue Rust more, simply because Rust's concurrency libraries (it doesn't seem to be an intrinsic part of the language like Go) seem to be more modeled after Python's or Node's. Go's `go` is even more readable than Erlang and Scala, so in my opinion it's one of the great three features of the language
(OT, but to me the other two major features are 1: channels being an integral part of the language, and 2: forced immediate error handling -- perhaps you disagree that this second one is a feature and prefer tracebacks, but in my experience large Go projects always seem to have cleaner code than, say, Python, probably because errors are handled very close to their origination. Lastly, there are a minimum of footguns in Go.)
But now that Java has the same feature in the form of virtual threads, I'm once again back to complaining!
https://softchris.github.io/golang-book/05-misc/04-goroutine...
Channels let you block (control suspension points) and wait for progress from any other thread.
Yes, good grief this is annoying.
Context would suggest you are referring to Go, but Go has exceptions. It even has exception handlers. As do all of the other languages mentioned.
a() -> Int() | Void
b() -> Int() | Void
for(a()) {
| na -> for(b()) {
| nb ->
print(na + nb)
// now what? From here, I want to continue to a(), not b()
I don't believe there is much use for coroutines without support for their interleaved execution.You have to liberate yourself from tick-tock synchrony.
There are 'a' and 'b' processes, they generate messages. You cannot predict how those will arrive. If you want them strictly alternating, you can tag each of the messages with the source, and block until you get the next other one you expect. Or you can just receive them all and make sense of them on their own terms.
Thank you for telling me which kinds of program I am not allowed to write any more in the bright new future of the programming.
As for the rest of your comment, I am aware of CSP and its derivatives, thanks. The question was about the language proposed in TFA that seems to be lacking the choice/select primitive.
It's a lot easier to read and write, BUT it comes with the downside that its not easy to save or load, which makes it only usable in certain game types and/or actions.
Can you expand a bit on that?
- Cuda: https://github.com/mratsim/weave/issues/133 - OpenCL: https://github.com/mratsim/weave/issues/134
https://en.wikipedia.org/wiki/Session_type
I think one difference is that session types capture which state a process/co-routine is in after receiving a sequence of messages whereas this system does not, it captures how one can respond to each state in isolation (e.g. I don't think you can statically capture that once a door is closed and locked you can only receive the locked state from it and respond by unlocking it).
It's syntax didn't help it's spread unfortunately.
The world truly belongs to those that can be both.
I agree with your take. In this case, we have one group that tries to explain the world using algebra while loudly endorsing that approach. Meanwhile, the other group just wants to write error-free state machines.
They are deliberately simple and for dataprocessing. They not meant/suitable for things like e.g. IO whereas the continuation monad is.
Everything is a tables and SQL!
(hint: tables and queries implement content-addresable memory [1] and, consequently, dataflow architecture [2], which is also Turing complete)
[1] https://en.wikipedia.org/wiki/Content-addressable_memory
Go lets users do this only over channels, so the rest of the ecosystem has to bend everything into the shape of a channel, including timers, cancellation tokens, etc.
Rust generalizes this to any kind of future, though the ergonomics get rough in many cases, sometimes requiring pinning and sometimes risking dirty cancellation with no way to account for the resulting state.
Under this proposal:
How would you allow multiple coroutines to progress in parallel while selecting on the next update from each of them? When you're selecting, you don't yet have anything to send any of them, and when you get something back from one, you might have to send something to one or more of them, but they're still working and are not yet in a state where they can receive.
How would you cancel coroutines while still allowing their implementation (not the caller's) to decide how to get back to a known state? I can understand if the intention is that you always have to send them a signal to indicate this, but as above they have to be in a state where they can receive it, and it will still be extremely likely that at least one caller forgets to cancel at least one coroutine. Especially since the purity of this approach means it must work recursively as well.
door() ->
| :open(:close)
| :closed(:open | :lock)
| :locked(:unlock)
What feels wrong to me in this example is that the type system doesn’t “know” that :close-ing an :open door will yield :closed. Expressed as a state diagram, it would only have a single node, whose type would be a sum type with three options, and the diagram doesn’t tell you which option you end up with for any given state transition, while at the same time you have to know the current option to know which transition you may take from the current state.So there is an asymmetry regarding the associations to the three sub-states between the two ends of a state transition. Or in other words, the state transition arrows begin in three different states but end in a single state that is the opaque combination of the three source states.
Virding's First Rule of Programming:
Any sufficiently complicated concurrent program in another language
contains an ad hoc informally-specified
bug-ridden slow implementation of half of Erlang.First time that I learn of the .onl TLD. I thought it was a typo but it actually exists for 10 years already: https://icannwiki.org/.onl
A function is some executable code. A closure is that plus some mutable state. A coroutine is usually a closure because something needs to track where it was suspended. Maybe this post is from the context of one of the languages that does the function colouring trick, where the answer is usually that said colouring is motivated by performance concerns.
There is a missing abstraction in the pthreads model - one can yield the current thread, but cannot yield to some specific other thread. I vaguely remember a patch set from google to add that to linux but am having trouble finding it now.
IMHO the invisible switch-case code transform only makes sense when stack switching isn't available (such as in Javascript or WASM).