You are viewing archived messages.
Go here to search the history.

Henning Sato von Rosen šŸ•°ļø 2022-03-16 09:24:02

Hi all, I wrote down some thoughts on colorless async. See below! Iā€™m not experienced in use of go-routines etc; I write from a language design perspective and, yeah so; I might be totally wrong/have mis-conceptualized something. Iā€™m in a process of learning to think about this stuff and would be delighted to have feedback/think together about the topic of designing async into a language!


Colorless async seems obviously great, but what if the opposite is also true?

I read this blog post What Color is Your Function?? on ways to integrate async into a language. Itā€™s an entertaining and convincing read by a super experienced developer. The article leads you through a chain of very obvious thoughts, that you cannot imagine even wanting to contradict; ending up with the understanding that the best solution is to enable using sync and async functions interchangeably, e.g. by supporting go-routines.

I was totally convinced. But then I thought, what if you go all the way in the other direction, embracing that sync and async are so fundamentally different in their semantics. What it that difference was present on the language level, in stead of being hidden by magic?

This has been done many decades ago and with superb usability results, in the form of state machines and most notably statecharts. ā€œRedā€ (sync) functions are transitions. ā€œBlueā€ (async) functions are states. Thinking of them and visualizing them as distinctly different is a game-changer for the human mind: A top-down understanding can be conveyed to non-programmers and programmers alike, etc. In the typical visualization, sync stuff is not ā€œred functionsā€, they are arrows. async stuff is not ā€œblue functionsā€, they are rectangles. Arrows and Rectangles composes perfectly, because of their differences. Itā€™s hard to see whatā€™s gained conceptually by merging arrows and rectangles into one thing.

Steve Dekorte 2022-03-28 18:14:01

Ray Imber "Isn't cpu scheduling a state machine by definition?" The point isn't that using stacks avoids state machines, it's that they do that work for you.

It's like automatic garbage collection. The point isn't that memory management isn't done, it's that it's done for you.

Writing your own state machines instead of using stacks is like programming with GOTOs instead of using functions. It's technically possible, and in theory may be more efficient, but there are reasons why it's no longer considered good practice.

Ray Imber 2022-03-28 19:18:02

Steve Dekorte

It's like automatic garbage collection. The point isn't that memory management isn't done, it's that it's done for you.

I don't think it's the difference between manual memory management and GC, it's the difference between reference counting and GC. Both pre-emptive multitasking and co-routines are automatic scheduling algorithms. Neither requires you to build the state machine yourself. The difference is how you influence the scheduling. Pre-emptive schedulers don't take any influence from your program. Whereas coroutines provide "yield" points that can be seen as hints to the scheduler to be more in line with the needs of your program.

is like programming with GOTOs instead of using functions

I don't think that's accurate at all. Are you saying all state machines can be represented as coroutines? This duality seems much more like the Church-Turing duality of lambda calculus vs. Turing machines. It's theoretically important but not practical by itself.

I'm also not convinced your argument is true if that's the case. Coroutines deal with a particular class of state machines (those dealing with scheduling), they are not appropriate for all state machines.

Are you saying that all state machines are better represented as cooroutines? If that's the case, then this is very clearly subjective. That is an argument similar to the difference between functional and imperative programming. There is no evidence that one is universally better than another. Some things are better represented functionally, and some better procedurally.

Anecdotally, I dislike the GOTO argument. It misses too much nuance, in the same way that "functional programming is always better" is a useless statement. Both Knuth and Dijkstra himself walked back the "GOTO considered harmful" statements.

Donald E. Knuth:> I believe that by presenting such a view I am not in fact disagreeing sharply with Dijkstra's ideas, since he recently wrote the following: "Please don't fall into the trap of believing that I am terribly dogmatical about [the go to statement]. > I have the uncomfortable feeling that others are making a religion out of it, as if the conceptual problems of programming could be solved by a single trick, by a simple form of coding discipline!

https://pic.plover.com/knuth-GOTO.pdf

Programming abstractions should be ladder that you can both walk up or down depending on the engineering needs of the particular problem. Dogma has no place in engineering imo. To be fair, we are all human, so that is more of an aspiration than the reality.

Ray Imber 2022-03-28 19:35:41

coroutines allow us to avoid the complexity of state machines (by using stacks) without making in impossible to write correct code (as preemption does).

...

This is unlike multiple threads preemptively writing on the same memory at the same time, which is an environment where even the world's top experts have been shown to be unable to write correct code.

Sorry, I don't buy it. Async lets you write more efficient code around IO scheduling. It solves a particular problem, that is completely orthogonal to correctness or memory safety. Coroutines are about "when", not "where".

Corountines do not solve multithreaded memory safety. Async and Parallelism are not the same thing. You can still have race conditions with coroutines (Go has had plenty of bugs showing this). Multiple Coroutines can still be scheduled on different CPU cores and write to the same memory. You need something like the Rust borrow checker to solve that problem. Not coroutines.

Coroutines also have the advantage (depending on how they are implemented) of having far smaller (and extendable) stacks than preemptive threads, which allows them to scale to several orders of magnitude more concurrency than preemptive/OS threads.

This is the problem coroutines solve. Coroutines deal with optimization, not correctness.

Steve Dekorte 2022-03-28 19:39:27

Ray Imber I'm saying that in most every case in which single stack asnyc is used (say, in Javascript) the proper (with i/o wrappers and a schedule) use of coroutines (i.e. multi-stack async) would result significantly easier to write/read/debug/maintain code (as well as less code). The larger and more complex the app, the larger this difference will tend to be.

Ray Imber 2022-03-28 19:45:03

I think this exists already: it's called Node.js, and, afaik, it did not make large complex apps easier to write/read/debug/maintain.

From what I've seen, adding richer type systems with things like typescript or one of the myriad of other languages that use javascript as a compile target have had better success on those fronts.

Steve Dekorte 2022-03-28 20:22:49

Node.js doesn't have coroutines.

Ray Imber 2022-03-28 20:36:35

Node.js has async, yield, and iterators. That's the required building blocks for coroutines. See the many coroutine libraries on npm for examples. All of this is based on CSP Theory from Hoare. Just varying levels of abstraction.

https://medium.com/@adambene/async-await-vs-coroutines-vs-promises-eaedee4e0829

Ray Imber 2022-03-28 20:46:11

Most Lisps implements coroutines the same way btw: async, yield, and iterators

Ray Imber 2022-03-28 21:33:01

Look. You can do some cool stuff with async and coroutines. But it's just another tool in the toolbox imo. In fact, in my experience, show that async and coroutines make debugging more difficult not less. Coroutines expose more things for the programmer to think about, not less.

Standard sequential architecture hides all scheduling from the programmer. The OS or runtime pre-empts your code at arbitrary points. It's invisible to you. You don't think about it. It's like Virtual Memory. Virtual Memory creates the illusion that you have infinite RAM (and the OS in the background pages memory in and out of your process without your program having any knowledge of it at all.)

But, there are downsides. A database really needs control over when things are paged in and out of memory. But I don't want all programs to do this. It makes life harder on the programmer. I want the option to pick the right tool for the job. For databases Virtual memory is the wrong tool, for some other application it's the right one.

Pre-emptive scheduling is the same idea. You write your code as if there was a single CPU core and your program is the only thing running. Then the OS will sneakily pause your program and let some other program run, you being non the wiser. This is a great abstraction!

Similarly, coroutines explicitly expose the scheduling points to the user. Preemptive scheduling has the disadvantage of only being able to proceed as a fast as it's slowest I/O call. If you are building a web server, preemptive scheduling is the wrong tool. You want to process a lot of I/O and not have have to wait for the slowest one, you really want something like coroutines.

But! now you have many possible orders of execution that you have to keep track of when debugging. The illusion of a single linear order of execution is gone. You have added more states to your DFA, not less.

I have some experience in this area. I am actually a big fan of coroutines! I spent a year working with the async team working to add coroutines and CSP to the Nim language. I read a lot of research on this stuff, and I have experience with the inner workings of both the language transforms and the schedulers. CSP has some cool properties. I'm not denying that. It certainly isn't some magic paradigm shifting solution to modern software architecture though.

Ray Imber 2022-03-28 21:42:44

Here is an example where (formal) CSP works really well. I would like to point out that it is used as an explicit tool to model state machines, explicitly modeling all the states, not hiding them:

https://www.reaktor.com/blog/why-csp-matters-ii-how-do-i-know-sync-works/

Ray Imber 2022-03-28 21:55:59

Another really cool use of CSP principles is delimited continuations, as seen in Racket: https://docs.racket-lang.org/more/index.html#(part._.Continuations)

It's a way to do AJAX style stateful request/response, using only a single handler. It combines server routing and I/O scheduling of the network call together. IMO this results in much more readable and clean code. Iirc Hacker News uses this trick, still to this day. I wish more modern languages and servers supported the technique.

Steve Dekorte 2022-03-28 22:00:57

Did you read the article linked in the original post for this thread? ( https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ ) I think it explains JS's async/await is not the same as coroutines.

Ray Imber 2022-03-28 22:25:10

I have read the article many times. Did you read my responses? Let me be more clear. JS doesn't have coroutines, but it has the building blocks to make coroutines. Ok, so that isn't at the language runtime level, but it amounts to the same thing.

From the article:

This is where the ā€œred functions can only be called by red functionsā€ rule comes from. You have to closurify the entire callstack all the way back to > main()> or the event handler.

What's the difference between "closurify the entire callstack" and "reified callstacks"?

An implementation detail, that's what. They are semantically equivalent.

You implement a coroutine scheduler using a trampoline at the main and a set of execution stacks.

Go is the language that does this most beautifully in my opinion. As soon as you do any IO operation, it just parks that goroutine and resumes any other ones that arenā€™t blocked on IO.

You know that "goroutines" are just a fancy closure right?

When you write the go program "read("file.txt")", it's essentially tranformed into "await read("file.txt")"

Steve Dekorte 2022-03-28 22:37:32

There is a difference between what is possible and what is practical. For example, if I try to use some open source JS code, will everything be written in async/await? Or will I have to rewrite it and potentially maintain the async/await version will future updates of the module? What about all of its dependencies? Do I update and maintain all of those too? While these barriers are theoretically surmountable given unbounded time and resources, they are practically non-starters for most programmers. Coroutines would remove these barriers.

Ray Imber 2022-03-28 22:37:59

If you want an example of something closer to Go, see my original Zig example at the top of the the thread. It works very similarly to Go but without the magic that hides it from you.

From the zig docs:

The point here is that the > amain> function, which is the demo of typical async/await usage, works in both an async context and blocking context. The programmer was able to express the inherent parallelism of the logic, without resorting to > function coloring> .

There is admittedly a bit of boilerplate in the example. > Here's the tracking issue for that> .

Now for the related > Standard Library> updates:

This introduces the concept of "IO mode" which is configurable by the > Root Source File> (e.g. next to > pub> > fn> > main> ). Applications can put this in their root source file:

I personally think the Zig solution is much better than the Go solution.

Ray Imber 2022-03-28 22:55:48

here is a difference between what is possible and what is practical. For example, if I try to use some open source JS code, will everything be written in async/await? Or will I have to rewrite it and potentially maintain the async/await version will future updates of the module? What about all of its dependencies? Do I update and maintain all of those too?

That's a good argument, though as much of a political one as a technical one. It's more practical from a technical standpoint than you might think though. It's possible at the compiler level to use program transforms. i.e. lisp style macros (what Nim and lisp do) or compiler passes (what Zig and Go do) to automate a lot of that, at least from the point of the standard library. Just make the standard library I/O calls async, and automatically transform the callers into delimited continuation closures. Admittedly that is fairly invasive and not foolproof (depending on how the caller code was written).

Steve Dekorte 2022-03-28 23:36:40

"It's more practical from a technical standpoint than you might think though..." If you write and maintain it, I would love to use (and advocate for) it - assuming it worked well with debuggers and didn't wreck performance. IMO this is the greatest weakness of JS at the moment.

Ray Imber 2022-03-29 00:35:13

You are right about JS. There would have to be a lot of changes there. It's most practical at the language level, where you can uniformly enforce the transforms. In terms of performance, reification of closures is something that can be heavily optimized. (I think V8 does a lot for this already, but I don't remember all the details). Debugging is another story. I have yet to see good debugger support for concurrent code in any language. That's the main reason I think concurrent code is not a silver bullet. If you had a solution for that, I would love to use and advocate for it!

Ray Imber 2022-03-29 00:40:27

You could run some "macro expander" program on top of your JS code and all dependencies. I guess during the "bundler" build step in the javascript world. I think something like this is what Vue.js does for their component language?

But at that point it's not JS anymore is it? You are basically building a new language that compiles to JS... Are DSLs that get macro expanded in lisp still lisp? šŸ˜›

Ray Imber 2022-03-29 01:20:57

I've been out of the JS world a long time, looks like React already added coroutines (specialized for react components): https://blog.logrocket.com/deep-dive-react-fiber/

Mariano Guerra šŸ•°ļø 2022-03-24 20:39:26

Do you have any example of a UI Widget type created in the last 20 years? (like slider, select, radio button, checkbox, things you would have on a UI builder palette)

sohalt 2022-03-30 22:19:11

I really like the time picker in Android (eg for setting an alarm). It's fairly similar to a radial/pie menu though.

Mariano Guerra 2022-03-31 09:30:23

Is there a "grammar/pattern/algebra of parser combinators" somewhere?

Something that describe the common parts that most parser combinators have, choice, iteration and so on

Paul Tarvydas 2022-03-31 10:04:34

A: PEG

but...

DSL for parsing == Ohm-JS.

Ohm-JS >> PEG >> parsing combinators (IMO)

older DSL for parsing == S/SL (Syntax / Semantic Language), superseded by PEG

SSL and PT compiler

Ohm-JS

PEG

(fyi, you might use Ohm to create source code for parsing combinators in any target language, not just JS)

Chris Maughan 2022-03-31 13:23:55

My favourite parser combinator implementation. You can optionally give it a grammar, and it will build the combinators for you. Very easy to use:

https://github.com/orangeduck/mpc

Jimmy Miller 2022-03-31 17:44:18

There are monadic parser combinators.

http://www.cs.nott.ac.uk/~pszgmh/monparsing.pdf

Walks through combinators that are pretty common

Deepak Karki šŸ•°ļø 2022-03-27 02:20:23

I recently came across this interesting piece of programmable tech : https://www.youtube.com/watch?v=ip641WmY4pA

Other than capturing my attention for being an interesting product, it got me thinking of new hardware extensions (or complete systems) that would enhance/change the coding experience.

We always discuss what kind of digital UIs would make programming simpler and more intuitive, so I was wondering if someone has looking into new kind of hardware I/O which would change how we program today. In fact new hardware could spur new mental models and digital interfaces for programming! (Think of how a mouse / touchscreen is central to something like Blocky/ MIT Scratch).

I have no experience in this field, just wanted to get a discussion going and see if someone with a bit more familiarity could chime in!

Florian Schulz 2022-04-01 08:06:31

Yes! Thereā€™s so much hardware involved in live music programmingā€¦ for software programming I think itā€™s an under-explored area. Of course, we usually donā€™t see software programming as a ā€œlive performanceā€ but I think thereā€™s potential.

Florian Schulz 2022-04-01 08:10:31

Also what comes to my mind are keyboard shortcut caps for hardware keyboards: https://logickeyboard.com/shop/final-cut-pro-3703p.html

Florian Schulz 2022-04-01 08:12:54

But as long as we need a keyboard to type text to write code, weā€™re stuck with that piece of hardware already. As soon as we program with higher abstractions, we could imagine dedicated hardware.

Eric Gade 2022-04-01 12:37:50

This is really cool!

Mark Dewing 2022-04-03 18:33:52

I'm interested in using call graphs/control flow in a hierarchical way to understand programs better. The problem is there seems to be two extremes - high level diagrams done manually and low level call graphs generated by tools. A manual drawing with boxes and arrows is often used when describing a program at the highest level. While it works, one question is how to move to the next level of detail? Someone has to do that manually as well. These diagrams aren't connected to the source and can get out of date. The structure is going to change slowly at the highest level and so keeping up-to-date manually isn't that much trouble. More detailed levels, though, can change more frequently and keeping them up-to-date is more work. At the other extreme, tools to generate callgraphs give all the functions. They can filter by time, number of calls or call stack depth, but those doesn't necessarily correlate to what's important conceptually. I'm wondering if there's any work on anything between these two extremes? Both in generating it and visualizing it. (Searching online for 'hierarchical call graph' gives research on automated and machine learning approaches to discovering a hierarchy - interesting as research, but not what I'm after here. I would prefer something manual like adding program annotations or creating filtering terms - something that can be automated as part of a build or CI process.)

Kartik Agaram 2022-04-03 22:37:26

I spent some time thinking about this a few years ago, but eventually moved on for a couple of reasons:

  • I had lots of different questions about codebases that required many different visualizations. I wasn't able to come up with a single map that's useful in all situations. Code has far more dimensionality than regular space so the map metaphor is perhaps not as useful. (Though I think zoomable UIs are under-explored.)
  • I observed that good tools immediately cause people to create larger, more complex messes that outgrow them. So I started questioning why programs get so large and complex, and whether that's a good thing.

For example, I now sometimes build little text-mode programs that anyone can edit while using them, and they come with a simple big-picture view that's just a stratification of the call graph: https://archive.org/details/akkartik-2021-11-14