A bunch of things you don't like about Rust? Turns out that the person who originally created the language doesn't like them either.
I know that the main point is about governance and how having a BDFL would have led to a completely different language but I really would have preferred the Graydon-BDFL-Rust to what we have today.
> Traits. I generally don't like traits / typeclasses. To my eyes they're too type-directed, too global, too much like programming and debugging a distributed set of invisible inference rules, and produce too much coupling between libraries in terms of what is or isn't allowed to maintain coherence.
Very much this!
> I wanted (and got part way into building) a first class module system in the ML tradition. Many team members objected because these systems are more verbose, often painfully so, and I lost the argument. But I still don't like the result, and I would probably have backed the experiment out "if I'd been BDFL".
I seen this the ML first class module system mentioned in a few different contexts and want to pick it up at some point.
May I ask what you don’t like about traits (and perhaps insight on what the author meant)?
Being a relative rust noob compared to other languages, I always felt traits were a super power when compared to eg interfaces in Java/C++ and flexible but with useful constraints compared to the structural typing nature of Typescript interfaces
Importing a trait into scope can silently materialize methods on other types, with no syntax at the method call site pointing to which import statement created the methods, requiring you to ask the compiler/IDE. This can also mean that removing a use statement with no references to the type being used in the entire rest of the source file, can make code suddenly stop compiling. I've worked around this by strictly confining "specialized" traits (like std::io::Write or std::fmt::Write, why are there two Writes?!) to the beginning of a single function, but I'm lazy and put "general" traits like std::borrow::Borrow and std::str::FromStr at the top of my file.
Which types grow new trait methods is dependent on trait implementations, which can be generic and apply to an unbounded number of types, based on complex matching rules, and figuring out requires reading every impl of that trait to see if any match a given type, or asking the compiler/IDE. I want to explore languages which explicitly select a trait implementation at the call site (like a Heap<int, int_greater> type which uses int_greater::cmp() to implement a min/max heap), or naming the trait (but not picking an impl) at the trait method call site (like Write::write_all(stream, "hello world") or (stream as Write).write_all("hello world")). I think Zig takes a similar direction.
There's worse. When your trait adds function foo and your code does obj.foo(), if the underlying type later adds a foo method, compilation breaks. This is very common with traits that usefully add useful methods that are missing in libstd... which break when said method is finally added there.
I ran into this case yesterday, for some cases there are warnings now. In this case I used ".div_ceil(...)" from from the Num [0] crate.
(some_integer).div_ceil(&2)
^^^^^^^^
= warning: once this associated item is added to the standard library, the ambiguity may cause an error or change in behavior!
= note: for more information, see issue #48919 <https://github.com/rust-lang/rust/issues/48919>
= help: call with fully qualified syntax `num::Integer::div_ceil(...)` to keep using the current method
= note: `#[warn(unstable_name_collisions)]` on by default
In a trait-like system (including Java interface), there can be only one implementation of a trait for a type. That's why it is global, unlike ML module-like systems. Global is another word for anti-modular.
For example, in the real world, there are multiple ways to order strings (called collation), but since there can be only one implementation of (String, Ord) type-trait pair, one ordering is canonical. That may be bearable, but having canonical (String, Hash) implementation is not. What if you want to use (say) faster CityHash instead of canonical MurmurHash? So Rust resorts to things like BuildHasher, to get back multiple implementations.
Because it is global/anti-modular, it interferes with separate compilation, and it is one of reasons why Rust is slow to compile.
Then why would one use trait instead of module? Since there can be multiple implementations in module, you need to specify. So module is more verbose. In my (and Graydon's) opinion, a bit more verbosity is worth it for modularity, but many people disagreed.
This is another theme: Graydon is okay with verbosity, boilerplate, and being bureaucratic. In my (and Graydon's) opinion, programming is work that is secretarial, not artistic, so it is unimportant whether code is ugly or not. You may think current Rust is ugly, but no, it is the way it is because lots of people really cared about Rust code being pretty. If Graydon was a BDFL, Rust would be even more ugly, and in my opinion, as a result, would be a better programming language.
Thanks for this - the sort example made a lot of sense to me.
I thought it was possible to parameterise with specific trait implementations for such a case though I haven’t used it much with I could be misunderstanding.
I loved reading that part! I've always found ocaml's (and now rescript's) first class modules flexible and powerful. They're definitely awkward but in that way where you're having to explicitly think about and declare relationships you'd happily ignore until they bite you.
Recently I've seen a lot of programmers I respect consider them a weaker or failed alternative to typeclasses, basically a dead-end. And I had been reluctantly coming to the conclusion that I must be wrong in some way I'm not able to fully perceive yet. Seeing such a notable PL designer come out on their side makes me feel less like a fool.
I often felt that being canonical was a pretty big advantage of traits. For example if you use first-class modules then every time you want a sort or a binary search tree you need to specify the choice of comparison operator for the elements. And a data structure for a search tree must somehow encode this in the type system so eg you can’t merge two trees with the same key type but different comparison functions. The most obvious way to implement this causes problems for other cases like writing functions generic in the types of keys/values (else you get extremely verbose code).
Canonical instances goes against modularity but something like modular implicits for ML-family languages could improve the terse was a lot to something closer to Haskell levels. You still miss out on the other advantage rust has: the dot operator is great because it allows identifiers to contain less context (they don’t need to have long global names or be hidden away in some tree of modules: you can have .map mean different things for different types) and gives a massive hint to autocomplete for which things to suggest (I don’t know how you even signal to autocomplete in a language like ML that you would like a function to operate on the following value so please suggest things with the right type).
Most type systems are not full-featured enough to elegantly encode traits/typeclasses or ML modules. You need dependent types, as seen in languages like Agda or Idris. And these in turn are hard to implement in a language with a traditional compile/run phase distinction like Rust: the _full_ feature-set of dependent types is pretty much only available at compile time. (Which is why dependent-typed languages tend to add "program extraction" features, which reintroduce that phase separation in a rather ad-hoc way.)
Committees tend to design less beautiful things. Look at modern c++ too. What a mess. Everyone in the committee has to jam in their favorite feature, much of which is sugar.
Modern C++ _is_ actually a mess to be fair. Overall it's still an improvement on pre-c++11, but when looking at it end-to-end it feels awfully disjointed.
If you have a mess and add something to it, you still have a mess, probably a bigger one. To clean the mess you need to remove something (which is really hard in a programming language) not to add something.
Selecting and enforcing usage of a subset of C++ is a way to deal with that mess which companies frequently use.
Sure, but that's why Carbon and Cppfront are a thing now. These two actively remove stuff from C++.
We'll probably see this happen in the Rust community as well once the newer Crab language becomes established. Then we'll have to rewrite everything in Crab, but it will probably be a lot simpler than the Rust rewrite.
They do so once in a blue moon, and only for ideas so spectacularly bad that either they were never implemented or never used by anyone. Those aren't the main problem with C++ mess, the issue is the many small but important details which are deeply embedded into the language since it started as a super-set of C.
C++11 is exactly where the mess started ;) C++ releases got more frequent since then, but at the same time, more half-baked features slip in that haven't been battle tested in the real world before, or need refinement over the next decade worth of releases (if they can be fixed at all).
I don't think that modern C++ is any less messy than old C++. If anything it's more so, as even more difficult conscessions to backwards compatibility emerge. That's not the say the new functionality isn't useful, but I don't think it cleans anything up. Modern C++ is approaching an order of magnitude more complexity than pre-C++11 (which is not surprising if you are adding features but not removing or changing old functionality), and I don't think it's a sustainable increase.
It suffers from the same problem C++ does though, it's got a broken "prelude" and the committee just doesn't have the courage to ever fix it. Weirdly a long time ago the opinion was that since the language was academic they weren't hindered by the demands of stability that corporate users might so they could do epic things like the switch to monadic I/O.
I feel if the Haskell committee could simply pick one of the alternative preludes, and gather some packages into a new standard library, and announce it all as a Haskell 2.0 or Haskell prime or whatever it could really reinvigorate the community. But what committee would be motivated to do such a thing, bearing the responsibility if it went wrong?
They are fixing it... it's just happening at committee speeds.
I would really like to be able to specify what prelude I want at the .cabal file. AFAIK, that's one change that wouldn't break anything.
(Anyway, Haskell is one case of a language asking for a fork. Maybe it takes the form of Idris or some other derived language having some sudden growth instead, but you are right, the committee currently isn't as fast as the community.)
I agree with the broken "Prelude". Fortunately, you can mostly ignore the Prelude and replace it with your own fairly easily. (As you suggest.)
The bigger problem is that strings are broken: by default they are linked lists of Characters. You can use better strings (like Data.Text) and you even get to use them as literals in your source, but the language ecosystem of libraries mostly assumes that you are using the default strings. And converting back and forth is annoying.
Because of all the existing libraries, it's harder to route around [Char] by yourself.
I am very glad to not have Rust end up along that path (I am sympathetic to the threading idea he mentioned though).
Rust as an alternative to C++ does, for me, involve all of the weird magical nonsense that you kind of need to get any of this working. The extreme use of generics to build out DSLs to get things working. General libraries being very hard to write, but still possible, to get alright ergonomics for usage itself. And yeah... the zero-cost abstraction thing.
I think Graydon-Rust would have also been very interesting, but it sounds unappealing to me, person who wants "C++ but nicer".
But to his point... saying "you could have much faster compile times" is very tempting! Just, especially when it's messing around with Rust "for fun", the ergonomics sound pretty unfun.
Go is actually a competitor with StandardML and loses badly in every way except having Google's deep pockets to carry it.
StandardML uses a far superior Hindley-Milner type system. It has pattern matching. It has Option types for good error handling. It doesn't have bad features (for GC'd languages) like direct pointers and slices. It has immutability by default. CML even offers a better take on channels too. Modules keep interfaces more standardized (superior for big projects IMO). And to top it all off, SML is easier to learn than Go. It's also as fast as Go (despite the compilers being a side project).
Go's only advantage is more extensive libraries, but that would be fixable in SML with just a fraction of the money Google spent on Go.
> Turns out that the person who originally created the language doesn't like them either.
But it also turns out the very same isn't as neurotically attached to his 'likes' as most of us are:
> The Rust I Wanted probably had no future, or at least not one anywhere near as good as The Rust We Got. The fact that there was any path that achieved the level of success the language has seen so far is frankly miraculous. Don't jinx it by imagining I would have done any better!
He understands that his likes are just contingent facts about a single mammal, not truths. This is a hard-won understanding. Most people never reach it.
> A bunch of things you don't like about Rust? Turns out that the person who originally created the language doesn't like them either.
Yep. That just goes to show that you shouldn’t put individuals on pedestals as if they are superheroes. More things than we usually think are in fact team efforts.
> I know that the main point is about governance and how having a BDFL would have led to a completely different language
That isn’t the only main point.
> > The Rust I Wanted probably had no future, or at least not one anywhere near as good as The Rust We Got.
If you would be fine with a niche language then that “Rust” would have worked for you. But if you also wanted a language with wide industry backin etc.—maybe not so much.
Very interesting insight from Graydon, in hindsight I too would have loved something more towards ML than C++. I never liked the kitchen sink approach that I see first C++, now Rust moving towards, but I respect what Rust has managed to solidify into. It's a good language.
That said, I still hate async with a passion, it makes the language more complex and not very elegant (i.e. function coloring). And now that I know how it works behind the scene (thanks to Jon Gjengset [1]), it feels so complicated and hacky, a mediocre very high level concept that someone managed to implement as a zero-cost abstraction. Impressive, but still a bad idea.
I'm sure the pro of having a BDFL instead of a committee is being able to follow a singular vision, instead of trying to appease members by adding the fad du jour which might stray a little too far from the original vision. Too many chefs in the kitchen and all.
Nim is the closest to what I had originally hoped Rust would be. Especially Nim 2 with the new GC is really exciting. I suspect it will outperform all but the best hand tuned allocators.
It's even hard real time capable, which makes it potentially viable in scenarios like games where GC traditionally hasn't been.
Function coloring article conflated two things: one was legitimate limitation of JS unable to wait for async result from sync call, and the other was just author's opinion how the syntax should look like.
Rust's async doesn't have the limitation author described – you can spawn and block both (Tokio has some limits there, but that's Tokyo's choice, not language limitation), making "color" largely irrelevant.
The second point was that both should have identical syntax, which Rust deliberately chose not to, because from Rusts perspective that would be too much implicit magic.
I disagree. You can call an async function in Rust from a regular one, that's true, but the returned value still needs to be passed through an executor to be useful.
Async without function coloring means being able to call `async fn add(a, b: usize) -> usize` from anywhere and having an usize back, whether you're calling from a regular or async function. If you need slightly different logic because of async, you got function coloring.
In fact, a hypothetical async without coloring functionality would not even need the `async` keyword at all, as all functions would be effectively the same, so you could choose to call one asynchronously or not with no particular ceremony. Is this even possible to do without compiler magic?
Thing is, it's a function coloring that mostly doesn't impede you by making certain operations impossible without massive refactoring. Regardless of an extra method call needed, it gets rid of the "you can only call a red function from within another red function" barrier by allowing you to do it, as long as you specify how (execute inside this thread, or delegate to a worker thread). This makes most of the issues associated with Javascript function coloring evaporate.
Fair enough. It's not a deal-breaker kind of coloring, my issue is mostly conceptual. It's simply not elegant as a construct, but perhaps it doesn't have to be.
My experience with asynchronous programming is brief, but it taught me that you will never have elegant parallelism. It's a completely different problemset, you need a completely different solution to handle it. But the ideal API wants to treat the problems as one in the same.
So the two camps I've seen on solving it is either
a) blackbox it and set some rules for implementers, inevitably leaving to some hidden and very nasty bugs on the user end or bottlnecks for advanced users, or
b) give implementers the full tools to setup, launch, and synchronize themselves, which may end up being slower than a single threaded solution if the user isn't adept already with parallel programming.
You inevitably always have bits of a) creep in, even for the most explicit solutions
Not knowing about function coloring is a stupid dream and has terrible implications on the performance of your code, which is infinitely more important than you losing 2 minutes having to figure out that you really want to `runBlocking { callThatBlocksForADamnLongTime() }`.
async functions without coloring means that the only warning you'll ever get that `calculate2Plus2()` actually ends up running a distributed BigQuery and writing the end result to disk, printing it to stdout() and parsing that result to give it back to you is... hopefully, documentation is up to date and you read it?
Async function coloring is not a problem. Async function coloring is a solution to "software developers are awful and will do awful things without any warning". If `calculate2Plus2` did not exhibit the write to disk behaviour in v1.0.0, but bumping to v1.0.1 does, I'd really want a warning that it does at least, and ideally a compiler error. The proper solution to function coloring is to have a pleasant API to interop between both worlds so that, at worst it's just a dozen characters more to say "yep, I really want to block here".
Async doesn't solve that problem. What you're asking for is some sort of expression in the type system of expected performance, but there's no tech that can do this. Consider that modern NVMe SSDs can do disk reads faster than many calculations over the content of what was just read. A function that changes its algorithm to have worse time complexity won't be marked as async but could break your app, adding a single disk read would require marking as async but probably won't break your app (especially if it's in a non-hot path), adding 10,000 more reads where previously you had one doesn't require changing your async annotation but might break your app, etc.
So the whole sync/async distinction doesn't really make much sense outside the context of single threaded UI toolkits, where indeed you have to read the docs anyway because doing some very CPU intensive work on the UI thread will block it even if no "blocking" APIs are ever invoked (and what is or is not blocking is somewhat arbitrary anyway in many cases).
There are type systems which can track complexity (e.g. http://gallium.inria.fr/~fpottier/slides/fpottier-2011-01-jf...) but that's still different to performance; async/sync tells us things like "touches disk" or "touches network" but not "O(2^n) might take a really long time".
Alternately, an effect system can communicate "touches disk", can make functions which are polymorphic to effects (e.g. map touches the disk if the mapping function touches the disk), and can distinguish different effects, unlike async/sync being anything-or-nothing. IMO async is more of an implementation detail (as "may suspend/yield" is not a very precise or interesting effect), whereas effects would communicate the properties that ohgodplsno wants to know.
Yes, but are there any effects systems in use outside of maybe Haskell? Effects seem mostly to be stuck in the research lab and have been for a long time.
I don't mean "effects" in the sense where you can have effect handlers which get delimited continuations and all; it'd just mark what could happen (like checked exceptions). But I can't think of any languages with that and not effect handlers; Koka, Eff, and Unison come to mind for effects, though their practical-ness may vary.
Verse has effects, which by virtue of exposing it to the entire Fortnite community probably makes it a bigger language than Haskell in terms of user counts.
Adding a disk read won't break my software, but it can certainly make my software non-portable. If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time. It's not even a matter of breaking the app actually, it's a matter of warning that "I'm going to go outside the realm of just your CPU and your memory". Sure, you could also mark as as async a function that adds two numbers. It's stupid, it's a JS thing to do, but you could. Except that you need to actively opt-in to being async in this case, as opposed to have it be forced upon you because you used a function that is also async.
Blocking has meaning in a lot more contexts, and being a consultant on JVM related topics, you should know that: it's the entire purpose of Project Loom, and Loom doesn't entirely get rid of colour coding either explicitly for that purpose. Loom wasn't made because the guys at Oracle have a deep love for JavaFX, but rather takes into account the server world, where you really want to know that you're going into another context, another computer, etc. The only time where the existence of async doesn't make sense is if your entire language and ecosystem expects everything to already be distributed. In which case, you've just switched the default color to async.
Finally, you chose to read async as the current JS/C# abomination implementation, but most of the sensible languages have implemented it as an effect: Kotlin has suspend funs, they don't return a Promise, but they tell you two things: they're going to touch something like the disk or the network, and if you really want to have them in a non-suspend context, you can either get a Deferred<T> out of them (and find another thread to run it on, and handle synchronization yourself), or run them on the current thread (and block everything).
Blocking in Loom is primarily about waiting for network traffic, possibly with extensions to file IO in future - it doesn't suspend if you do file IO today. Loom does get rid of coloring, or rather, doesn't introduce more of it and lets you phase out what exists, so I'm not sure what exactly you mean by that.
Kotlin suspend funs do not tell you they're about to touch network disk, that's the reason they use "suspend" and not "async". Suspend funs can also use generators (with yield) in which case no I/O is happening but they are nonetheless suspending.
So this is why blocking as a concept isn't a great one, IMO. The Kotlin designers were right to not use the word in their implementation of coroutines. Where Loom discusses blocking a bit, it's not defined as being about blocking, it's about being able to scale up threads to way beyond what was previously possible. It just so happens that the primary reason you'd want to do that is if you have lots of threads that spend lots of time waiting for things, but that doesn't automatically require network or disk access. For example you can use threads that spend all their time in Thread.sleep if you were writing an agent simulation.
> possibly with extensions to file IO in future - it doesn't suspend if you do file IO today
Wasn’t basically all of the JDK’s file APIs rewritten to io_uring-like calls to support Loom? I have thought that IO was definitely something that Loom handled.
>Adding a disk read won't break my software, but it can certainly make my software non-portable. If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time. It's not even a matter of breaking the app actually, it's a matter of warning that "I'm going to go outside the realm of just your CPU and your memory". Sure, you could also mark as as async a function that adds two numbers. It's stupid, it's a JS thing to do, but you could.
You're conflating too many orthogonal things.
It's not merely because of the performance of touches disk/network that async was used for those cases, it's because that waiting is not because you're held up by the language doing calculations. That isn't the case with a function like you describe.
Marking functions async when they aren't yielding just to signal that they might be slow is a bizzare idea. That's not what async is and it doesn't bring any real benefit, it's just abusing the notion (and limiting the contexts where you can run those functions). You'll still be using libraries which wont follow this strange idea, and you should know their performance characteristics.
Blocking code exists in all major languages, including JS. In a single thread context, having something "async" wont help you at all, if it calls anything blocking, which can be something as common as JSON.parse
>If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time.
All of those have nothing to do with async, and what async is created and used for.
What you want is something like Haskell's IO "tainting", a (side) effects system, or something to that (no pun intended) effect.
This is a perfect solution fallacy. Yes, "whether this function will suspend (and shift execution onto a different thread)" does not tell you absolutely everything about that function. But it tells you things that are worth knowing, that you want to be visible (not super intrusive, but visible) in your IDE. And in a language with a decent type system the cost is pretty small; functions that are agnostic about whether they will run as async can and should simply be polymorphic in async-ness.
This comment reads like it's from an alternate dimension where "calling read(2) marks functions as async" is a common language feature. Over here, read(2) can block forever and normal functions can call it in just about every language.
I agree that hiding coloring entirely is really a trap. I want to know when some IO work runs because it means potential exceptions unknown with unpredictable latency in my program.
As a design choice of OCaml’s eio - an async runtime, you must pass around a dependency to all your IO functions. With the “net” object passed visible in my function type signature, I can tell some network work may occur. The first benefit is we get synchronous Go-like code.
1. Low enough that you don't need to worry for most web backend code and 2. Anything is easier than that. *XYZ is not Send* amiright?
With Rust's async you have to worry about a different worker thread picking up the work after a context switch, which makes things complicated. Not so with Java.
Its certainly possible with compiler magic. Zig does exactly that - where functions can be colorless and they work slightly differently in async and syncronous contexts, doing the obvious thing in both cases. I'm not sure how well it works in practice (I haven't spent enough time with zig to know). I'd worry that bugs would creep in to whichever variant of the function you aren't exercising.
_Unyielding_ describes the issues that arise when the author of the code doesn't have control of the transactionality / atomicity of their operations at the level they need to: https://glyph.twistedmatrix.com/2014/02/unyielding.html
To my mind it's very much a balancing act between "low power to the developer, high power to the language" and "high power to the developer, low power to the language" all up and down the stack from software / hardware to "consumer / framework".
TL;DR - he says that "doParallel" and "doConcurrently" are separate operations with distinct semantics that designers of programs must care about and that conflating the two (especially the common "doConcurrently-and-often-but-not-always-in-parallel") is one of the most common causes of bugs in programs that need to make progress in multiple threads of execution.
A common thing about writing like this article is that it confidently declares futures/promises are good for parallelism, but this is wrong - they are flawed because they introduce priority inversions.
To schedule tasks properly, you need to know who is waiting on them as early as possible. With a promise, you only know when you get to the "wait()" call, which is too late.
The correct solution is called structured concurrency.
Depends on if you're using Monix/Haskell style tasks (lazy, run when depended on) or JS-style promises (E Promise / C# Task / Java Future - run when instantiated, waiting is post-submission). Agreed completely though, structured concurrency is fantastic.
You can do it using a runtime with scheduling and fibers.
The issue is that you want to be able to save the entire thread stack cheaply so that you can switch to another task at an async yield point, and then go back to your previous stack frame. You want to do this without spawning system threads.
Say you have an f -> g -> h call stack that blocks on IO. If all the functions are in fact async state machines (or some other kind of callbacks) your thread stack will look like this:
Executor.loop() -> Executor.run(h) -> h.await()
That is, your functions are really objects that are queued up and taken as needed by the executor. If h has a yield point, you can put it back on the queue, go back to the loop and pick up some other work. Later you can do the above again and await h to again up to the next yield point.
Now consider the case where the middle function g is NOT async. In that case the call from f to g will be a normal function call that gets put on your call stack. In a language where this is allowed, you'll still have to wait or execute h and you'll get a call stack that looks like,
(where our awaitUntilCompletion is minimally just calling h.await in a loop until h is finished). At this point, you are stuck: you can't yield from g() because it is a normal function with a normal stack frame, so your thread has to wait till g() finishes before it can be used for anything else. You are basically blocking a thread on h. At this point you might spawn a new thread to keep your thread pool count up (which is expensive and memory hungry) or just accept the forced synchronous blocking (which reduces throughput).
If this happens over and over again, you either end up running out of memory or end up running code in a synchronous fashion but with worse performance (because of all the state machines). This is why functions are usually coloured, so that you don't get yourself in this accursed state by accident. AFAIK the alternative is to allocate all of your call stacks in the heap so that you can switch them in and out of kernel threads, which is what fibers are. This requires a complicated runtime with a scheduler such as in Loom,
Algebraic effects like how OCaml’s now implementing it may be possible! The ecosystem around effects in OCaml is still really young, but here’s an example of an http request being made that is asynchronous, non-blocking but looks synchronous with no special syntax.
Performing these effects is similar to throwing exceptions up the callstack where whichever ancestor handles the IO work, then resumes the child with IO work done in hand.
As someone who likes OCaml, but hasn’t touched OCaml in ~5 years, that’s a very hard example to read. I can tell it’s making a network request, but I have no clue:
- what the type of res is (is it a string? A buffer? Async string? Something else?)
- how this code does not block: what is happening in parallel? At which point will it block?
I would like to be excited about OCaml’s algebraic effects, but right now I don’t really get it.
- It's some type from which you can read a response body. In OCaml we often work in terms of abstract types i.e. the operations which can be done on types. If you want to see the exact name of the type you can always look up the signature of `Client.get`: https://github.com/mirage/ocaml-cohttp/blob/16e991ec1f7e5f0c... (nowadays LSP support in any good editor will show the type on hover)
- It's implementing non-blocking I/O using effect handlers. The complexity is not exposed to the library user, which is actually the whole point. If you want to dive into the concurrency library (Eio) and study how it works, that's very doable, just like any concurrency library in any other language.
There's really not that much to get–OCaml will do async I/O without having to colour functions, just like Go, Erlang/Elixir, and soon Java. It's not something sexy like monads that excite people with mysterious FP concepts, it's just a lot of hard work that went into simplifying how developers code at scale.
Yawar’s explained it all, but I’ll share tidbits of my own.
When reading ocaml on _text_ like from GitHub, I personally accept that I won’t need to know the type and underlying data structures just yet unless I read and have open the .mli file.
I’m excited first and foremost because we can have async code feel synchronous like Go.
How does the code run? IO runtime is defined in userland, like Rust’s Tokio. That’s the “Eio_main.run” part. Underneath, eio spins up and manages processes in a non blocking manner, even scheduling work.
Since effects are essentially resumable exceptions, this particular http request throws an effect to the runtime. The logic and this particular process pauses (okay I may be wrong here). Logic flow now continues somewhere in our runtime level (the effect handler), where all the async IO work happens. The runtime resumes the child process and returns the http result back to the child via a “delimited continuation”.
You can't evaluate a future in normal rust as there's no default executor, you need to pull in some library to even make blocking calls to async functions.
It's a bit strange indeed. On the other hand you can't allocate either if you don't have an allocator. The difference is just that the allocator is opt-out while the reactor and executor are opt-in with no formal default.
It's the same in C++. The compiler and language design came first and then the library (aka executor) part comes next.
Rust has basically 2 executor libraries, tokio and async-std. It seems to me like tokio is solidifying as the executor of choice and it's only a matter of time before that design is baked into the std library.
In C++ there is a default executor that is part of the standard library and has been so since C++11, e.g. no external library is needed - std::async.
std::async is mostly "good enough" for ordinary cases where you only want to fire-and-forget and have no control over the underlying dispatching and scheduling algorithms. If you want something tweaked towards your use-case, and hence actual high-performance executor library, std::async is not gonna cut it. And that's where the "C++ executors" proposal come in.
That's...not precisely true. The C++ standard doesn't specify how std::async works, and for a while GCC just ran the operation sequentially, and later both GCC and Clang launched new OS threads by default. https://stackoverflow.com/q/10059239/1858225
I've been using C# and (Java|Type)Script pretty much since they were invented. Both use a function coloring async system. I don't know what it's like in Rust, but at least in the two examples with which I have experience with this apparently much-maligned system, I really don't get the complaints. Having to "color" functions really isn't that big of a deal.
JS has first class functions that you can pass around. The different colors debate was just a confusion - there where only one type of function, just that some functions took a function as an argument, althgough JS has no async functions. All async functions in JS comes from the runtime, like addEventListener for web DOM events. The "solution" to the non existing problem the committie came up with was to introduce 3 new types of function. So JS went from one type of function, to four types of functions.
His dislike for functional constructs didn't do Python any favour, but now that he's gone I see Python is adding the kitchen sink as well. I haven't kept up with the language since that pattern matching proposal.
People who are deeply into programming languages seem to like FP too much. That and theories of type systems. Python is doing some great things, but also just fucking around a lot.
As someone who doesn't like Python. Python's problems have almost nothing to do the lack of functional programming. It does have the worst lambda syntax of any programming language but that is about it.
If they're all terrible, then the language won't appeal to anyone and will die off naturally.
On the other hand if everyone gets to add their favorite terrible idea, there may still be enough non-terrible parts to appeal to enough people to keep momentum.
The transition was slow (Python 2 was supported in parallel to 3 for a long period of time so there was more than enough time to migrate), relatively easy to do, and brought huge benefits to the ecosystem.
It could have been done and forgotten in 2 years if some community members had not been dicks about it.
It really seemed that the issue was mostly "Unicode is really hard if you haven't thought _deeply_ about it" because application code, library and framework code, _and_ "system" Python code had to go through several iterations of "here's how to do it right" before something reasonable came out that solved issues for most / all stakeholders. And the Swift / Raku people are still sitting on the sidelines smiling, knowing that there are a lot more "lenses" to view text through that Python doesn't make easy (but neither does Haskell, JS, or Java, so there's good company in good-enough-for-now land).
JS devs have been transpiling for years now and it works.
Python should have released a conversion tool that could automatically convert most stuff and identify the stuff it couldn't convert. This kind of directed upgrade would have made the process much easier for developers to actually accomplish.
It's important to note that JS has an unfair advantage in that if you want to write code that runs on the universal platform, you're forced to use either it or something that transpiles to it. Python reached where it was on merits alone.
(That said, JS is actually a very versatile and almost-great language, getting better all the time)
Most universities I knew were still teaching Java well into python's rise to popularity a decade ago. Python got where it is by focusing on being simple, easy, and nice to use. Seriously take a look at PEP-20 sometime:
> That said, I still hate async with a passion, it makes the language more complex and not very elegant (i.e. function coloring)
Function coloring seems to come up a lot in these discussions, but I don't see a better way without providing a runtime. Could you propose an alternative approach to async, without sacrificing ability to write zero-overhead, high-performance bare metal systems?
The Rust async story would be much nicer if they'd put in the hard work up front to support higher-kinded types, as then it could have a monadic async API like OCaml or Haskell. I don't know anyone who's used async in both Rust and Haskell who prefers the Rust approach. It'd also fix oddities like why it's possible to write a function like the following in C++ but not Rust:
template<template<typename> class MyContainer>
int getFirstInt(const MyContainer<int>& myContainer) {
return myContainer[0];
}
(In Rust it's not possible to write something like MyContainer<int>; only the int is allowed to be generic).
Are you aware of generic associated types, which landed in 1.65 (late last year)? To a considerable extent, they’re Rust’s answer to higher-kinded types. Less expressive in some ways, but fitting Rust way better, as basically a relaxation of a former restriction, rather than a new feature. If I’m reading your snippet right, GATs let you express exactly that, though minus `template`’s duck-typiness so you’d have to spell out what contract the container must adhere to.
>less expressive in some ways, but fitting Rust way better, as basically a relaxation of a former restriction, rather than a new feature
They're not expressive enough to implement a monadic async library, as far as I'm aware, so they don't fulfill the role of proper HKTs in this context.
>minus `template`’s duck-typiness so you’d have to spell out what contract the container must adhere to.
Do you think it's consistent that the T in MyContainer<T> can be "duck typed" but not the MyContainer?
In Rust neither is duck typed. For T you must specify the traits it must have. This makes compilation fail at definition, not at the point of incorrect usage.
For an example of GATs, you can see the following:
> They're not expressive enough to implement a monadic async library, as far as I'm aware
AIUI you need GC in order to do that. There's a reason why Rust's async/await support needs compiler magic.
(I'd like to see standard interfaces for "pluggable" garbage-collected arenas in Rust, but this will need to wait until after Allocators/Storages get fully stabilized.)
I really wish people stopped using this concept, especially in the context of Rust because the `async fn`/“regular function” split is strictly equivalent to the `try_something()`/`something()` split (the first one being failible and returning a `Result` in case of failure). `Result`s and `Option`s are coloring the stack in exactly the same way a `Future` does (and `async` is pure syntactic sugar on top of future).
So, someone may like exceptions and green threads more than `Result` and `async` (and this is a completely valid PoV, even though I personnaly like the explicitness better), but thinking `async` is somehow special is just a conceptual mistake.
BTW, in the original blog post the “red” functions were actually functions with a callback parameter, which is actually very different.
> There's consistent use of Result and Option in std,
Well, there are dozens[0] of incompatible error types in std, two of them being just called `Error`[1][2].
Everytime you create a function that use more one of the different error types from std (for instance, an I/O error and a TryFrom-related error) you have to create a custom Error enum to accommodate the different kinds of errors from std…
> community agreement to do things that way
And the community agreement is e̶r̶r̶o̶r̶-̶c̶h̶a̶i̶n̶, ̶f̶a̶i̶l̶u̶r̶e̶, ̶ s̶n̶a̶f̶u̶, this_error, unless it's for a binary crate and then it's ̶a̶n̶y̶h̶o̶w̶, ̶e̶y̶r̶e̶, color-eyre.
The error ecosystem is much more fragmented than the async one (even though I think this_error is here to last, so I don't have to migrate once again…). In the async world, there's pretty much tokio (but I think it would actually be better if it interoperated better with other runtimes so alternatives could emerge)
> A lot of it is much harder to read.
Ah yes, fut.await is fundamentally harder to read than res?, right?
Overall, you vastly underestimate the friction that the `Result`-based error handling adds. I still think it's worth it, and it's been slowly getting better (did I say I loved this_error?), but it remained one of the biggest point of friction in Rust over the 8 years (time flies) I've been using it. Async is comparatively less disrupting (mostly because it only appears in the call stacks where you're doing I/O, whereas errors are ubiquitous).
I think a big part of any successful endeavour is alignment of authority, responsibility and competence. The problem with group decision making is responsibility is very diluted. If a bad decision is made, no one in particular is responsible.
I hate async as well. Developers should have learned about communicating sequential processes and blocking queues and none of that would have been necessary. It creates a weird divide in every language. Just learn about threading and do it.
Yep, it's pretty alarming that a developer would toss away async when threading isn't a fit for all situations - what happened to the right person tool for the job? As a professional, I 'hate' some things too, but I'm not going to blindly make my work harder and less efficient out of some small preference I have.
async has fragmented the crates ecosystem. If you want to want to write async-free code with threads, you'll probably still have to opt-in to an async executor for some dependency because these days many libraries will implement only an async interface
I think you're manifesting what I'm talking about. The reason you cannot replace async with threads has nothing to do with the ecosystem, but the simple fact that threading is a model of parallelism and async/await is a model of concurrency. These are two subtly different concepts in software architecture, but they are not the same thing.
More concretely, threaded code has nothing to say about whether or not its synchronous or asynchronous, and asynchronous code has nothing to say about whether or not it's evaluated in parallel.
> I think you're manifesting what I'm talking about. The reason you cannot replace async with threads has nothing to do with the ecosystem, but the simple fact that threading is a model of parallelism and async/await is a model of concurrency.
That is an artificial distinction you made up. I have written many apps and desktop applications, all of which where using 3-10 threads, some of them doing "parallel" work meaning the parallel computation of divide and conquer algorithms, some were doing concurrent work.
I tried async often. I threw it away every single time, because it "infects" the entire codebase. I suspect the only reason async exists is because the Javascript event-loop is single-threaded and there are no blocking primitives in JS. If you don't have access to blocking data-handoff between threads, you can choose between callback hell or async/await.
It's not an artificial distinction at all, but it is subtle.
2, 4, even 256 sockets exchanging data can be worked with concurrently on a single thread and gain performance vs blocking and waiting for the first socket to finish. There's no parallelism, since they're never actively reading/writing at the same time, but they're concurrent because they exist and operate in overlapping timeframes.
Running two independent algorithms could mimic this - run part of algo A, then part of B, then A, etc. It's not useful for performance, though. To be useful you require parallelism - you have to have the algorithms executing at the same time, using multiple threads.
On async itself - it's not perfect, but honestly, there are contexts where it makes a lot of sense. I work on a lot of non-blocking C code - the entire programs are basically epoll and timer driven. As a result, the high level coordination is just callback hell. Async is very nice in comparison.
Yes, you can spin up threads to do everything with blocking, but non-blocking I/O came around specifically because the threads add overhead and kind of suck. It's worth noting that having threads can also infect the codebase. You either have to carefully manage mutexes, or you only communicate with channels and have to worry about keeping things updated and in sync. Sometimes this works great with minimal communication between threads. However, if you have one socket per thread and the sockets are all triggering actions that mess with the same data... it's not so great.
They should have fixed the overhead of Threads instead of changing the languages. Fibers/virtual threads eliminate this overhead, Java can run millions of them.
Besides IO Completion Ports for async IO has been the fastest way to do heavy IO on Windows, and it was introduced years before JavaScript even was a thing.
And that's just one example. So hardly think JavaScript was the main driver there.
Rust Channel have the nice additional that they don't panic, and don't have all the weird Go Channel Axioms[1] that you have to wrap your head around to get things to work correctly[2].
For what it's worth, I don't think Java Virtual Threads (a.k.a. project loom) has implemented an explicit concept of channels, although I guess BlockingQueue could work as a channel.
There is a big issue with bringing CSP to languages like Java 21 or Go, that use so-called "colorless concurrency" or "stackful coroutines". CSP losses a lot of its modeling power when you combin shared mutable state and pre-emptive (or invisible) parallelism. And this is exactly what both of these languages do.
"Colored" abstractions like async/await are more cumbersome (especially in Rust), but they let you know exactly at which points of execution your stackless coroutine may suspend, and you can handle synchronization accordingly.
Those functional concepts are cumbersome, but threads ("green", "kernel", "virtual", "goroutines" or otherwise) are not the panacea their proponents claim them to be. I had to debug way too many data races in my life to consider them perfect. Rust still has a lot to improve on its async front (no async traits, async closures or async iterators and the async runtime confusion), it can at least promise me that I won't get any data races.
> it can at least promise me that I won't get any data races.
But not any other kind of race condition.
Also, data races are “safe” in java. Plus you can rely on immutable data for those parts. Parallelism will always be hard, there is no model for shared mutability that would be easy.
It's crazy that the programming community even accepted the concept of async/await as a sane one.
Being sync or async is essentially a property of the attention of the caller, not of the action itself. Is "eating a donut" a sync or async action? If I'm focusing all my attention on it, essentially putting all tasks aside (after) - then it's a synchronous action. If I'm reading a book/watching a video/walking/etc, while eating – it's an async action.
How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?
Is "network request" a synchronous or asynchronous activity? It depends whether your code blocks and wait's until response (or timeout) or continues executing and handling it when it comes. It's property of the "attention" of the caller, not of activity itself.
But "can be gracefully executed asynchronously" is a property of an action.
Something that pegs the CPU to 100% because it's doing intense processing isn't a good candidate for async. Similarly, some code can cause issues when written in a non-streaming fashion. Take the following example (in python):
x = [x for x in range(1_000_000_000)]
y = (x for x in range(1_000_000_000))
If you spawn a bunch of async workers doing the first one concurrently, you'll OOM your system. If you spawn a bunch of async workers doing the second one concurrently, your system will be fine.
In other words, "async" is a label on a box of donuts that implies (though doesn't ensure, people can of course still do bad things) that the donut won't explode if you look away from it.
Hmm, is it a post-factum rationalization or it's the original logic behind async/await? Let's mark "heavy" functions with a label, so user has to call them differently not to overload the system?
Even if this is an original logic, why language is deciding for me what is considered heavy or not? What if I'm fully aware that I'm doing heavy processing and I want it to be happening in the background. What if I'm writing HFT software and every call is heavy for me. Language is not a right level of abstraction of marking "heaviness" of the code.
It really just doesn't make sense. Why stop there and not start marking functions with how many times per second they can be called? Like you can call "light" function 100 trillion times per second and OOM the system, so let's mark it "func light() async 2_times_per_second {}". It only can be called from functions that have lower per second label.
Plus, if Python cares so much about not OOM-ing the system I would start by not requiring 32-bit int to occupy 28 bytes in the first place.
>is it a post-factum rationalization or it's the original logic behind async/await?
Well, I'm not that deeply versed into any language's history, but I imagine any 15+ year old language had a point where they needed asyncrhonous functionality but had to write around legacy code. I don't think Javascript would have been written the way it was in the 90's if asynchronous operations were a mandatory priority.
So it's probably a lot more "this is how we hack this in without creating Python 3.0". Relatively elegant to make it an optional part of the language that you explicitly choose to delve into, instead of a core feature that would have broken thousands of sites behind the scenes if you made it "right".
>why language is deciding for me what is considered heavy or not?
because we decided decades ago that we didn't like using fork(), nor creating/destroying processes ourselves. problems that would inevitably need multiple solutions when supporting multiple platforms because these aren't language features so much as OS calls made in a language that you may or may not be writing in.
Remember at the end of the day that languages are abstractions on top of an OS. Any interesting ideas for problems involving more than a single process space that your OS makes for you is bound by what the OS's API lets you do. It's arbituary but also based on historical problems to wrangle.
So we haven't had enough problems where we need to bound a function by how much it is called. We have for how to interact with all the above OS/language problems.
It's only functions that "block and wait" that it makes sense to be async - the point being that blocking and waiting doesn't use up CPU cycles, so you might as well free the thread up to do other stuff. If a function issues a socket write, then continues utilising the CPU while also periodically checking if data's available to read, it's effectively doing manually what async does for you, though I'm not sure there are too many real life examples of such functions. But pre-async it was certainly common enough for block-and-wait functions to tie up a thread and hence execution of programs with limited multi-threading (even today, GUIs often require all events and updates to be processed on the primary thread).
How about waiting until all or part of donut pack is eaten by multiple eaters? Jokes aside, async/await comes from easier handling of a callback code and automatic function splitting. Writing asynchronous donut eating in loops or with yielding of partial results is very easy to swallow with such syntactic sugar. The whole model is very easy to grasp and to work with. I'm not saying that's the best solution but it definitely works well.
Regarding stickiness of function colors - it never happens when async/await is used correctly (in the same principle as IO monad isn't sticky).
Doesn't matter. You either wait until donut is eaten by others or you don't. Caller is deciding if it's "synchronous" (blocking) action or not.
The question is how you translate mental models from real world to the code, and async/await fails here spectacularly. It's just weirdly unnatural thinking about sequencial processes. It requires tremendous amount of cognitive gymnastics just to reason about simple things.
I’m sorry but isn’t this exactly how async/await works from the perspective of the caller? If you want to wait until the donut is eaten you `.await`. If you don’t, you do other stuff and then join the future later. The fact that the executor can get other stuff done while you’re awaiting the donut consumption is largely invisible from the caller’s perspective.
That's only if you marked function with "async". Developer who writes code for "eating a donut" supposedly has better knowledge of how I'm supposed to eat it - synchonously or asynchronously. So if they didn't mark function as "async" I can't decide how I should eat it, it's decided for me.
You can eat it in the same thread. Some frameworks do that implicitly (even without await) . You can also control if it's consumed in particular thread or thread pool and decide which thread should continue. More, you can chain and/or parallelize it with other async and not-async functions without any effort. Additionally, you can handle errors and exceptions same as it's async and not-async function. Every good async/await framework offers that out of the box. More, some frameworks abstract executors and you can run the function remotely or even persist it with context. Of course you can achieve all that without async/await, but it makes that extremely easy.
> How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?
Of course it does, read it as "beware, something blocking down the road".
If you can EatADonout without blocking, please do, but want it or not that's a different implementation, one that doesn't block and the signature it's telling you so.
We're so used to sync and having hidden blocking operations. I wonder if in an alternate universe the first languages considered the blocking/async nature of operations and then some newer languages considered hiding this information into seemingly sync functions would produce similar but opposite outrage against it.
> want it or not that's a different implementation,
Implementation is the same. In both cases it's the same set of CPU instructions, but async/await languages create artifical division, forcing developer to think othervise.
So, let me explian my reasoning. Code starts with a developer's mental model of a problem and behavior of the system and then translating it into the code. The more straightforward this translation, the more readable the code. Code is a second degree map of problem domain so to speak ("real" world -> mental model -> code).
Like if you want to add two values, the simplest form of code would be "add(2,2)" which is pretty straightforward. If the code forces you to do some mental gymnastics (i.e. "2 2 addOnlyEvenNumbers") - that's less straightforward, less clear and less readable.
In the same vein, if you want to execute some function ("EatADonut" or "MakeHTTPCall") – you may care or not care about blocking and waiting for results. But it's your call. So it makes sense to give you two options to run this function. Go has simplest possible solution – "eatADonut()" vs "go eatADonut()". It doesn't matter what is a "default" here – it could be "eatADonut()" (go to background) vs "sync eatADount()". What matters that "eating a donut" is just a set of instructions to the CPU, and it's up to caller to decide how you want to execute it in terms of concurrency.
Now, "async/await" approach turned this ownership of "synchronicity" around. Now function is deciding how it should be called. Mental model of "actions" now needs to be translated into "actions being async or sync for the purpose of fitting into this language concept". Which is cognitively expensive for no added benefit.
Sure you can rationalize it, and get used to it as to any other absurd design, but it still adds unneeded complexity to the code, makes it less readable and less clear.
>In both cases it's the same set of CPU instructions
It's actually a very different set of CPU instructions. the function EatADonut is the same set, but "async" means the kernel needs to take time out of its execution to do some action as small as accessing an open thread, or as large as "gain access into a completely different piece of hardware" before putting that set of instructions onto that different thread. Not the program, the actual scheduling process between your program and the OS you are executing on. Then it needs a way to to get that result and sync it back onto whatever thread spawned it and access the result.
It is in fact a huge action, so marking it with Async is basically a very explicit warning.
>Which is cognitively expensive for no added benefit.
On the contrary, I can't even begin to imagine the amount of compiler optimizations it saves on as well to have that be explicit in code. I'm sure Go has to do all that on the fly while a colored language gets to allocate all those potential processes before the program runs. It's only no added benefit if you dont care about performance. But to be frank, you probably don't need more than a single thread if your problem isn't bounded by perormance. Parallel programming is all about getting something done faster after all.
>How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?
You have to think of it from a higher level.
Nobody knows if eatdonut is strictly async but everyone knows all IO calls are async. So something like socket.send would be async, this is obvious.
Then anything that calls socket.send would then in turn become async itself. The async sort of spreads around like a virus. Any function that uses other async functions gets polluted with the async marker.
So in this sense, you should think of it like this. If eatADonut is async it means it touches IO. It sends data somewhere else to some other thing. It also means it's not unit testable.
In a nut shell This is the intuition you should have about async:
Async Tells You about the properties of eatADonut. eatADonut doesn't tell you anything about whether or not it's async.
This is largely identical to how the IO monad works in Haskell. Unless you want all your logic polluted with the IO monadic wrapper or the async marker you have to learn to segregate your functions into async functions and non-async functions.
Under this intuition, EatADonut, by good design principles should NOT be async. This is what it should be:
The async marker forces you to organize your code in this way, You have to do this unless you want all your functions to be polluted by the async marker. This is the proper way to organize code Anyway, as it allows your code to be unit testable. Async calls are, in general, not unit testable because the functions are not pure. So if you look at what I did above, I segregated eatAdonut Away from IO and made part of the logic of the overall code testable via unit tests.
IO calls should be very general and small functions while your other functions should be pure and unit testable. This is the overall intuition you should have about async.
Believe it or not, this specific aspect of async is actually an improvement over golang style concurrency. Not an overall improvement, it's still debatable which style is better, but I'm saying the async marker is actually a feature that other styles of implementing concurrency don't offer.
Basically, async encodes the concept of IO and function purity into the type system. It allows the type checker and You to tell which functions touch IO and which functions are pure and unit testable.
People think unit testability is some kind of marginal benefit because you can cover testing with integration tests which are basically tests that happen across IO. It's more than that. The ultimate dream of programming something as if it's a bunch of modular lego blocks that you can elegantly place together is most fully realized with pure functions. Async enables more of this style of modularity in Rust (but not without some cost which is why it's debatable whether or not it's better than go style concurrency).
Do you mean on a hardware level? Because otherwise, hell no.
If I’m writing a binary file parser that buffers some data and processes it, I sure as hell want to wait for IO, there is no other meaningful way forward in most cases. It is also not a small isolated part, but a tight loop bouncing between CPU and IO.
This was my first (and obviously wrong) mental model of how async works.
There are functions, you can call them sync or async if they handle IO or UI or they will get a necessary data later.
I still don't understand why I can't fetch a URL from top level javascript. Also I don't understand why zig async and await passes the control flow that seemingly total arbitrary way. The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way?
Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript? Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines? I don't know. Anyway async as is used with colors is totally against the picture that the words "async" and "await" suggest.
> I still don't understand why I can't fetch a URL from top level javascript.
> Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines?
Yes, they are promises.
> The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
> The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way? Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript?
You're describing preemptive multitasking. You can create separate execution threads pretty easily in most languages (even super old C code), but that's not why Javascript and Zig have the 'async' keyword. The difficult part isn't the asynchronous execution; the hard part is handling the result from asynchronous code (i.e. the 'await' part). Promises are a simple mental model for organizing how pieces of code depend on the results of other pieces of code. The C# documentation does a good job explaining this:
Note: From Zig's documentation it sounds like 'async' is cooperative multitasking. This is single-threaded execution. It is "concurrent" code, but it isn't executing in "parallel".
Concurrency is difficult because it is explicitly resource management. You don't need concurrency for calculating the correct answer, you only need it for managing time.
Does blocking I/O or not is very much a property of the function itself, as is "may finish up running on a different thread from the one it started on" (or, if you prefer, "needs to be run under a runtime that provides that capability").
Sending a letter and getting a reply is an inherently async action; you can stare at the mailbox all day but you probably don't want to. Waiting in line at the bank is inherently sync; if you try to do something else and then come back they'll make you start over again.
You can see the async keyword on a function definition as a hint to the caller, that the action will be IO bound rather than CPU bound.
Caller can use this information to invoke 10000 IO bound tasks in parallel, whereas cpu bound tasks are limited to number of cores and may want to be throttled. In your analogy, I can eat a donut while simultaneously listening to music, but I can not eat a donut while simultaneously eating fish soup.
Now where it get complicated is when a function does both io wait and heavy cpu processing. Now neither color of the function match.
Forcing this upon the caller to invoke such functions in special ways is the strange part. Taken to the absurd you ought to have special syntax for every resource constraint in the computer, memory bound, disk bound, wifi bound, and so on. It would be better if the runtime could figure this out by itself and parallelize accordingly.
The single threaded aspect of async functions does have certain other benefits, like not having to care as much about muxes when synchronizing state.
Async/await, at least in JS, is here to make asynchronous code look like synchronous code, as in you can say "let toto = await asynchronousFunction(); doSomething(toto)".
Without it when you want to do asynchronous stuff you either block on it (not good since JS is single-threaded), eg "let toto = asynchronousFunction(); doSomething(toto)" and nothing can be done during the time asynchronousFunction is waiting on IO or something (in a browser environment that means part of your website stops responding to user input); or pass as an argument to the asynchronous function a function that will be executed by the asynchronous function once it "wakes up" (usually called a "callback"), and while it's waiting, the JS runtime can execute other stuff, eg "asynchronousFunction((toto => doSomething(toto)))".
Callbacks were I think from the start in JS. After a while promises were introduced, which compared to callbacks avoids nesting, and probably have some other advantages that I don't know about. Still, they are method chaining and not "regular code". For example regular try/catch won't work as usual, you have to use .catch(). Even later async/await got introduced, which allow you to write asynchronous code as if it was regular code, with a few exceptions (for example you can only use await in an async function, top-level await took a while to land on Node, stuff like that).
I don't think using real world human actions helps with understanding any of that. Async functions make sense in JS because again, JS is single threaded and if you do a blocking call (like asking the kernel to read a file, or making an HTTP request and waiting for the response), the event loop is blocked. From Node.js documentation's on the synchronous function in the fs module:
> The synchronous APIs perform all operations synchronously, blocking the event loop until the operation completes or fails.
> [Async/await] feels so complicated and hacky, a mediocre very high level concept that someone managed to implement as a zero-cost abstraction.
I feel like I need to point out that Donald “Structured programming with GO TO statements” Knuth included an example of coroutines in the first volume of The art of computer programming, the first edition, dated 1968. In assembly language for an accumulator machine. With a box of scraps!^W^W^W^W^WThat is to say, that C and most other languages have made coroutines awkward and thus virtually unused in the past three decades or so does not mean they are particularly novel or high-level.
Granted, Knuth used coroutines in a simulation and not for I/O, so he did not need that much of a scheduler, but still.
A zero cost abstraction is just an abstraction you couldn’t write any better yourself, not one that is actually free. If you’re lookint for an item in a hashmap, you still have to pay the cost of looking up the key. The promise of zero-cost abstractions is that you’re paying the lowest possible cost to do that, with a nice interface that you didn’t have to write from scratch.
Agreed, that's why I said might. Whether rust async is the best possible abstraction probably depends on the event loop, and your code. I could be wrong but I think if you write a trivial async function it will still have spinach for the async even if there are no yield points and it's effectively a non async function
If Rust was a higher-level language, then I'd say yes, just automatically handle running Futures in parallel, join them when you actually need to resolve the data, and pretend that they look like synchronous functions in the 80% of cases. Though things like `select!` wouldn't make any sense mixing the two.
I continue to find the "function coloring" argument misses the point unless you're arguing from a developer experience perspective. Why should two fundamentally different things look and function the same? Want this the ultimate pitfall of early RPC implementations where everything looks synchronous?
In Rust, a lot of the friction is due to how async functions/futures fundamentally differ in model of execution and how that interplays with the rest of the language. Other languages get to hand-wave a lot of these problems away with a GC. (It certainly could be less of a pain nonetheless.)
- Futures don't execute until polled, can partially execute if something isn't ready (and would block), can partially execute and be arbitrary cancelled and dropped. There is no equivalent in sync functions unless you make them co-routines.
- Since you can "pause" futures if something would block, you need a place to store the function's state for when it's resumed. Now you must be consider if the async function's state is `Send` if the future wants to move to a different thread to continue executing -- which is why you see `pin!` used. Sync functions don't care about this since you always run to completion on the same thread, and the stack is ephemeral.
- Likewise, the `Future` returned by the async function is going to need to encapsulate it's state that it saves. In the general case, this is a compiler generated anonymous struct that changes if any state saved across `.await` changes, hence the opaque `impl Future`. This is why you see `BoxedFuture` a lot to abstract this away at the expense of an allocation. Ideally, the new associated types with lifetimes can avoid this with traits.
So if all functions were co-routines (i.e. functions that can be resumed and re-entered) they would all have the same "color". But all you really did was "lift" all sync functions to be "async" functions with no internal await points.
(IMHO, if the C# team back in the day decided to implement full blown co-routines into the language instead of just `async/await` as a compiler trick, I think many other projects would have followed suit with a more general co-routine solution instead of treating `async/await` as this special thing which is just a specific compiler generated implementation of co-routines.)
> That said, I still hate async with a passion, it makes the language more complex and not very elegant (i.e. function coloring).
async in Rust is a popularity issue even more than a technical issue.
A whopping amount of people who are coming to Rust are doing so because they want a good ecosystem for implementing network service servers. Rust/cargo/crates hits the sweet spot for them.
I am with you that I loathe async/await in Rust. However, I also have to acknowledge that without async/await, Rust is a vastly less popular language.
All the other uses of systems programming are simply dwarfed by number of people building network services. That's just the sad reality.
As someone casually following Rust and haven't touched it or C++ in probably 5+ years (but keeping tabs in hope of coming back to that world), I feel like a lot of decisions he mentions are what made me optimistic about Rust adoption/positioning itself as a modern C++ replacement. So I kind of agree with his conclusion - his version of Rust would be much less interesting to me, and I think a lot of it's current users. eg. I only started playing with Rust once they removed green threads. Zero cost abstractions are a major selling point.
I was at the sidelines when Rust 1.0 was being made and I think it got into an llvm induced feedback loop. Slowly turning into C or C++ with other features but the same type, object and memory model.
Part of the reason was Rust's desire to show itself as a direct competitor w.r.t performance, I think.
The safe thing Rust did here is affordable in Rust (Option<T> is the same size as T for many T including all references) so they could afford to do it, whereas it's expensive in C++. Could it have been made cheaper in C++? Sure, but safety wasn't their priority so who cares?
That prioritisation applies to the whole ISO language, not only to the standard library.
You turned on a feature which helps diagnose this type of mistake at runtime, and it helped you by diagnosing the mistake at runtime. What does that prove?
What I'm talking about is that Rust's Option is very cheap in all the cases where it can be very cheap, which makes this whole design feature more affordable. C++ eventually grew std::optional which is not powerful enough for this work and yet is also bigger and slower. They could have done better, but safety wasn't a priority.
It proves that there is a way to diagnose this type of mistake, that is what matters.
Unlike C, which the only way to be safe is not to touch it at all.
As for performance, ISO doesn't implement compilers, there are many ways to improve performance while keeping the semantics in line with the standard.
If the compiler vendors decide to focus elsewhere is another matter, a bit like there are languages as complex as Rust and compile faster, because that has been a concern, whereas rustc developers have their concerns elsewhere.
It doesn't, and that would be a bad thing, because warnings aren't magic and bug-free themselves. Nor is there any reason to comply with every single warning every person in the world has come up with.
And what should be defined behavior? There are quite a few choices that depend on particular situations. Feature designers have no knowledge about what would you want so they left it up to application programmer to check the situation upfront and act accordingly to their wishes. If you want same behavior across the whole application you can always write generic function doing just that.
No. In Rust check is there by default, with optional unchecked access. In C++ the safety is off by default and you have to remember to check.
It's like Yaml parsers, they had "load_yaml" which is unsafe and "safe_load_yaml" which is the secure option. Imagine no surprise when Rubyists went for shorter safe looking method and got their servers pwned.
I think you are wrong. I looked and in Rust doc it says that: "Removes the last element from a vector and returns it, or None if it is empty". You better be doing check for "None".
The check is still there, that's not under dispute. GP is saying that it's still not the "same thing" as C++, since Rust will refuse to compile the code unless you either perform the check, or actively opt into unchecked access. In contrast, C++ will happily compile the code if you unintentionally neglect to perform the check.
for some reason I can not answer your subsequent reply so I do it here:
I looked at your example and played a bit with it and yes I agree with you - compiler does help in this case.
My old text:
So instead of checking if vector is not empty you check that the return result is not empty. I do not see much difference. If Rust compiler would choke when "else" clause in your example is not present I would understand your point about compiler preventing improper access.
`x`, the value of the Vector access, only exists within the context of the first block. It does not exist in any other scope. This makes it impossible to access when the result is not valid.
> If Rust compiler would choke when "else" clause in your example is not present
The compiler won't choke, but it will stop you from accessing the value.
It doesn't matter if you omit the `else` clause or not, the type system ensures that you can't access invalid values.
> for some reason I can not answer your subsequent reply so I do it here
Hacker News will try and discourage really quick back and forth comments. To do so, the reply button is hidden. However, if you click on the timestamp, to go to the comment's own page, you can reply there.
To be precise, Rust will refuse to compile code that accesses the value without checking that it is not empty.
Rust does that with the combination of "sum types" (aka "enums with data") and pattern matching. An `Option<T>` is either `Some(T)` or `None`. Matching on an option with the pattern `Some(value)` creates a new syntactic scope where the value is accessible. This scope is only entered if the `Option` is actually non-empty.
All ways of getting the value from the option are ultimately shorthand for a match in that way. For example, option.unwrap() will either get the value if it there, or panic. Option.unwrap_or(x) will either get the value of it there, or use x instead.
In practice this is x100 less error prone than C++. Source: was burned by vector.front() on empty vectors more than once. In C++, UB. In rust, usually the "empty case" is considered when first writing the code, or at least caught during review (unwraps tend to be very visible in reviews)
Problem is, you call pop on empty vector in C++, you get nasal demons. Not a case in Rust.
I'm not a C++ expert but here is my understanding. Vector is essentially a tuple of (dynamic_array_address: ptr, size: size_t, capacity: size_t). To pop a value from vector you just decrements size. So what happens when you have empty vec? Your size is 0, and you're substracing 1, which causes undefined behavior.
Correct way is to check BEFORE you pop_back().
In Rust, any number of invocation of pop() will not result in undefined behavior. You can ignore the value, or you can check it, but it doesn't expose you to UB just for slightly misusing a vector.
Absolutely and this is exactly what I do. I always check the containers before removing elements.
>"In Rust, any number of invocation of pop() will not result in undefined behavior. You can ignore the value, or you can check it, but it doesn't expose you to UB just for slightly misusing a vector."
I do not understand "slightly misusing" part. I assume in Rust it would bomb if pop() returns "none" and you try for example add said "none" to some number.
Someone much more insightful than me pointed out that most of the safety advantages of Rust are really a cultural phenomenon, rather than a strictly technical one. You could write unsafe unsafe Rust that derefs invalid pointers all day but when building systems and libraries with Rust, people value safety and Rust enables that as a priority.
It is also what attracted me into C++ coming from Turbo Pascal and Turbo Basic, back in the early 1990's.
Although C++ culture could be much better towards safety, it is definitely better than whatever WG14 is doing, or C has brought into the picture for the last 50 years.
Also anyone that just copy pastes C like code into C++, is the kind of developer that will be using unsafe{} all over the place, on the languages that have them.
Telling people to stop using unsafe is much easier than telling people to not have undefined behaviour.
C developers like telling themselves that only people with bounded rationality make security critical mistakes. All the skilled C developers have ascended beyond the mortal realm and would never let themselves be chained up with crutches for the weak like affine types or overflow/bounds checking.
> Someone much more insightful than me pointed out that most of the safety advantages of Rust are really a cultural phenomenon, rather than a strictly technical one
It's not. It's a safety phenomenon. See Java, C# etc.
Java doesn't have huge focus on it, but managed to get it right.
It's not an error by default and more importantly it's not an error under right circumstances (e.g. lets say number of pops and inserts is supplied via arguments).
Yes defaults matter, it doesn't change the fact that protective gear is available, and like seatbelts, helmets and motorrad full body armour, it is up for security conscious people to make do of what is made available to them.
It's not just that defaults matter, the problem here is why have footgun as the default?
In a runtime example I can run it with tests and it would behave fine if both values are same or first arg is bigger, in Rust's case it would behave valid for ANY combination of arguments.
Heavy disagree. This is less giving a safety belt and more like saying we got airbags (that are deadly without seatbelts) and a DIY seat belt is somewhere in there too (it's in a box under the seat).
Coming from Java the C++ stuff is jarringly unsafe.
Agree that Java is much better than using raw C++.
Still, if you want to contribute to OpenJDK internals, they will only be taking pull requests in C++, not Rust.
So better learn how to build those seatbelts and airbags, for all the projects we rely on, including Rust's own reference implementation, that aren't going to rewrite their code from C++ into something else.
> It's easier to work with than C++, but that's fairly faint praise.
So, while Rust positions itself as a C++ alternative with all the complexities that C++ developers love, it may become less attractive for other use cases. For instance, I find it highly questionable to develop a web app in Rust...
We build a proof-of-concept backend replacement for what is essentially “SharePoint being used as a DB with a frontend by people who don’t know how to use SharePoint” and was a nice experience. We also build it in a few other languages, C#, Go, Python and TypeScript and Rust was probably the best experience of them all. We ended up going with C# because we needed Odata, and at the time we hadn’t yet run into the many “joys” of working with Odata, ASP and Entity Framework and how their model builders, really, really, really, won’t play together nicely.
Knowing what we know now, we should’ve gone with TypeScript and just written our own Odata filter on top of it, but live and learn.
If I was in a position where I could pick and chose languages, and not worry about how not having TypeScript in most things will mean our best front-end developer can never go on vacation because he’s sort of our only front-end developer, I wouldn’t mind using Rust for web-backends.
Rust GUI is obviously not a great experience. At least not yet. But it’s not that bad either. I think it’s mostly the case of how JavaScript is just so good at it and seeing such a fast pace of improvements because the entire world uses it for most GUIs these days, that it’s just hard for anything else to compete. I mean, look at stuff like Flutter or Blazor, they are backed by Microsoft and Google and they’re vastly inferior choices for most use cases compared to simply building things in React, ReactNative or even electron, and that’s not because I have some wild love for JavaScript, it’s because it’s seeing rapid improvements they dwarf it’s competition simply by being used by a lot of people.
I wish Rust would have someone like Facebook pick it up and build a frontend framework for it, but I think that is just too unlikely for you to bet on, and you certainly wouldn’t want to do it yourself, even as open source because then that would probably be your entire job.
On the flip-side, the packages that handle basic back-end web stuff for Enterprise use are rock solid in Rust. Which is impressive, at least to me, considering it’s young age. I have no idea why, but maybe some serious players are contributing to it because they use it themselves. There isn’t a “Django” or Ruby+Rails for Rust, but if what you’re building is a lot of smaller APIs with various transport methods and data access in an federated authentication scenario then Rust is surprisingly mature for the web. It’s primary disadvantage being that your TypeScript and Rust developers won’t be able to cover for each other (which is why we didn’t poc with Java).
As someone who recently started their first web app in Rust and is using Yew for the front-end, I wish they included benchmarks against other Rust frameworks. I also wish I had more experience and had looked harder; Yew is very verbose in my experience and Leptos looks a little more sane.
> it may become less attractive for other use cases. For instance, I find it highly questionable to develop a web app in Rust...
I mean who'd want to and why bother trying to support that use case? Basically none of Rust's value proposition exists for a web app while nearly every single one of the downsides do.
Hard disagree. Rust brings a lot to the table for a web app. Compiled vs interpreted makes it easier to catch (syntax) errors beforehand. You can even go as far as compiled templates. Mapping JSON (or whatever) to strongly typed objects is great. Dependency management is best in class. I rather like diesel.rs (although it is very much an acquired taste) especially if you treat it like a safe query builder rather than an ORM.
The single biggest problem I've run into is that there's no real good story for a web app framework (especially since everyone's gone crazy over async). Rocket is perhaps a bit too magical for rust folks and it's been abandoned, but I rather liked it. I've warmed up to axum, but all this async stuff still rubs me the wrong way.
Perhaps. But TS is still Javascript under the hood which means implementation details leak and they smell awful. If they didn't it'd come down to a preference for compiled vs interpreted.
The number one syntactical beef I've is operators. e.g. == vs ===, ""+number.
Although all of those things are true, I think the main point being made is why choose Rust for that, instead of Go, .NET, etc, any of which offer everything you've outlined as being worth having, while having fewer of the downsides of Rust.
They don't, that's the "problem". Off the top of my head dependency management in Go is not best in class to put it charitably. .NET will tie you more closely to Windows. Sure, mono is a thing but you'll have more packages to choose from and fewer compatibility issues running .NET on Windows.
To me, one thing missing from most popular web languages is the single best part of Rust: A well-defined handling of the non-happy path.
I'm talking about things like exhaustive "switches", built-in, ENFORCED handling of "missing" values (null, undefined), and finally useful error handling.
I actually don't need any of the baremetal features of Rust. It's just that most of it's "zero-cost" abstractions are still far superior to those of other languages.
Yeah, I ask because the JVM world has had that since... whenever Maven's central repository came about [0][1][2], I guess, and it ensures that there's no debate about namesquatting [3], nor typosquatting attack vectors [4].
I am constantly baffled that NPM, PyPi, crates.io etc. didn't copy this idea for those last two reasons. In my mind, it's not quite best in class without it.
Or Java, F#, Scala, Kotlin, Haskell. If we were to randomly pick a language, chances are it could be a good fit for these — Rust became as well-known as is because it was made for a different niche, where there were no competition.
Hell, even Python can map JSON to a strongly typed object. I am a firm fan of Pydantic and its competitors when dealing with JSON blobs. No-one deserves a KeyError in prod code.
>"I mean who'd want to and why bother trying to support that use case"
I write my web apps in C++. They tend to be little bit more than just query database / update database. They're exposed as JSON based RPC and can be accessed by JS front end from browser or third party systems we interact with. The performance is stellar and the code size is not much different comparatively to using PHP / Ruby / Python / your_pet_goes_here.
>So, while Rust positions itself as a C++ alternative with all the complexities that C++ developers love, it may become less attractive for other use cases. For instance, I find it highly questionable to develop a web app in Rust...
But we already have plenty of options for this use case (.NET, JVM, Go, Python, Ruby, node.js, PHP, Erlang, etc. etc.). Very mature ecosystems that solve a lot of different edge cases.
There are very few C++ alternatives worth mentioning, none as mature as Rust in terms of adoption/tooling.
If you need to write a webapp that would require C/C++ kind of memory handling/performance then Rust would be the ideal candidate I think.
I don't know what kind of tradeoff matrix makes you use the Rust graydon describes over the existing options.
For example my main issue with D (but TBH that's something I've given up on actively tracking 10 years ago probably) is that by including GC in the runtime/stdlib - it basically painted itself as a poorly supported competitor to C# rather than a C++ replacement.
It makes me smile that my top-level comment says I would have preferred the ML-version of Rust, while you say you prefer the zero-cost-abstraction version of Rust that's more C++-like.
Indeed in software engineering there is no silver bullet nor a perfect language for everybody :-)
ML-version of Rust would not have been a uniquely interesting language. It targeting the C++-niche is what made it into quite the big name it has become.
At least Rust has left the door open to allow panicking on overflow even in release builds in the future - presumably on platforms where hardware support makes this cheap enough.
The correct (but hard) way to prevent overflow-related bugs would be to insist on the compiler being able to prove that no overflow can occur. Basically the same thing you do in your head to convince yourself that the program is correct and won’t overflow, only in a more formally rigorous fashion. Modulo semantics by itself doesn’t prevent bugs.
Rust has an optional clippy lint that will warn/forbid all basic arithmetic that might over/underflow, forcing you to use dedicated methods with explicit behaviour instead.
It's very annoying , but I use it in code where this is critical.
> And then you may just have introduced side channels in crypto code.
incorrect crypto code. If overflow is intentional, it should be annotated as such in operations, will generate similar assembly and won't panic. If it isn't intentional, then the code was bad to begin with.
If the branch can be mispredicted, and the misprediction happens depending on internal state you don't want to leak (cf. Spectre), then you have a side channel even if the branch is never actually taken.
A really unknown and underrated language for how good it is right now is rescript. It's ocaml's type & module systems and runtime semantics grafted onto JS. Ocaml's tooling, ecosystem, and standard lib situation are all ...fine... but quirky in ways that can be barriers when learning it.
Rescript lets you skip most of those, plus the (overblown but also real) weird syntax. I like ocaml and use it for projects still but if you just want to play around with what's unique and strong about it, rescript is what I would recommend right now.
I’m very glad expressivity won out. I would like our profession to stop accepting tools that waste effort.
I’ve always seen safety and lifetimes and borrowing as the main value prop, so I was surprised to see he was sort of aiming at an ML without GC, rather than a C++ that doesn’t blow up.
Ah, Sather gets mentioned. That's a name I haven't heard in a long time. I remember looking at it during the latter half of the 90s when I was searching for the perfect OO language...
Sather was something I was first exposed to at university in the mid 90s, and I still have a lot of respect for its looping mechanism. Code inclusion in classes rather than inheritance was also pretty cool. Pre/post conditions, invariants. I miss a lot of it.
I recently revisited it for Advent of Code (where part of the challenge was getting the compiler itself to even build with even semi-modern tooling), and a lot of the above features still have value.
"Benevolent dictator for life (BDFL) is a title given to a small number of open-source software development leaders, typically project founders who retain the final say in disputes or arguments within the community. The phrase originated in 1995 with reference to Guido van Rossum, creator of the Python programming language."
Interesting. Isn't a BDFL needed in Open Source projects (not talking specific about Rust, but Open Source in general)? I work in a commercial software company. We have a CTO and he retain the final say in disputes or arguments. He also steers the global technical direction we take the software and has the final say, but he can get fired. Is that the main difference between a CTO and a BDFL?
The usual alternative is a committee, see for example Apache Software Foundation's Project Management Committee Guide: https://www.apache.org/dev/pmc.html
I suspect an informal grouping (i.e. more long-standing/senior contributors with default control over some parts of the codebase and operating by informal consensus between themselves) is probably at least as common (maybe more so) than an officially named committee with rules of procedure.
The main difference between a CTO and a BDFL is that if you disagree with a BDFL, you can just fork the project under a new name. This right to fork is key to making FLOSS development work. If you disagree with a CTO, there isn't much that you can do besides buying up the whole company (see Elon Musk and TWTR as an example).
Of course Guido ended up not completing the "For Life" part -- neither his life nor the life of the language. (I think PEP 8016 is the one that started the steering council to take the place of the BDFL? https://peps.python.org/pep-8016/)
A euphemism for a software project dictator where the moral superiority is baked into the title. The dictator does not have to be benevolent in practice.
Technically, Rust had no future prior to the 2018 edition. The fact that Rust can add new features as the use cases for it evolve is a strength of the language, one that it had from the start even with Graydon as a BDFL.
Can someone comment on "Library-defined containers, iteration and smart pointers"? I have no rust experience so far. Magic compiler support for such primitives is something I usually very much, really strongly dislike, it was always my experience that it is the wrong point of abstraction because it just reduces the design state so much. But then you need good support for inlining the relevant parts of the language, which I think is very often very poorly done. In fact, I have so far never seen a solution I like.
What is the rust experience wrt to inlining? Can every expression be inlined or only selected ones? How can you know what got inlined in some expression? Do you have to manually annotate every single function call you have to inline or is there a more general command?
Everything that's visible to the compiler is subject to automatic inlining. That is all code in the current crate (compilation unit), all concrete instantiations of generics (regardless where defined), and all functions marked inline (regardless which crate).
Stdlib containers are all in the generics category.
> all functions marked inline (regardless which crate).
Is this a problem is rust? Is too much/too little marked inline? What if you really need to inline some function from some library that was not marked inline by the author?
If containers, control flow and so forth are expressible as library code you get a simpler compiler and stuff determined users can reasonably debug, modify, replace. It means you have to improve the core language enough to implement them and/or have magic compiler intrinsics which are only intended for use by that library.
If you implement these things in the compiler (open code means emit the implementation inline as you go, can also emit calls to the compiler runtime which is roughly similar to library code that the compiler ships and knows lots about) then users need to hack the compiler to change them.
However, if the structures are in the compiler, and you've done things like encode them directly in the AST, the compiler has a better chance of emitting useful diagnostics for them and of optimising them at the semantic level of the container.
C++ goes with library code supported by compiler intrinsics, and a common developer experience is compilation errors referring to iterators some distance into the library code. It also can't sanely do things like call reserve on a vector outside of a loop, because by the time it's ready to optimise things it's holding raw pointers with mangled names, not a hashmap instance.
Conventional wisdom is to put containers in the stdlib. D has some support in the compiler. I'm starting to think this is one where conventional wisdom has got it wrong.
Haven't followed Rust too much, but I'm always surprised when I hear that Rust is too difficult or not ergonomic. As I understand it is meant to be a systems level language; something you'd use to write kernels, TCP stacks, browsers and ssh daemons.
Anyone writing these things today in C or C++ already understands object lifetimes and Rust just adds a static checker for them.
In such projects churning out lines of code is not the bottleneck, ease of development should not be prioritized over long term maintainability.
Why on earth would you try to rewrite python CRUD apps in Rust?
>Anyone writing these things today in C or C++ already understands object lifetimes and Rust just adds a static checker for them.
As someone who only used Rust casually - understanding object lifetimes and knowing how to encode this in Rust type system is not the same thing. Not to mention that Rust can't statically prove some things that are valid (eg. cyclic references).
> Anyone writing these things today in C or C++ already understands object lifetimes and Rust just adds a static checker for them.
I've certainly seen seasoned C++ programmers saying this. Of course, a few of them of say they understand object lifetimes so well, they they don't need the static checker!
> Why on earth would you try to rewrite python CRUD apps in Rust?
This is one of the great mysteries of our times. I do think the enthusiasm for using Rust for web apps and such (a) is misplaced and (b) has been a drag on Rust developing into a better C++ replacement.
> Of course, a few of them of say they understand object lifetimes so well, they they don't need the static checker!
There is a difference between understanding lifetimes and being able to keep track of them. I have a lot of objects in my more than 10 million lines of code. Most of them have simple lifetimes that are easy to track, but a few for reasons (which may or may not be valid - often the reasons are it was built in C++98 and updating to modern lifetimes is hard when it is used all over) have complex lifetimes that are tedious to track. It isn't that I can't, it is that I get bored/make mistakes and the static analyzer wouldn't (Or course C++ can't be statically analyzed, but if it could the static analyzer wouldn't fail for the same reasons I fail)
Being able to stop constantly keeping things in the back of your head and just trusting the compiler to complain if something is off was the biggest differentiator for me by far. Less footguns = more better
When I switch to C++11 and unitue_ptr things got a lot better. There is still a lot of cruft from old code, but C++ is a lot better as of 12 years ago. I don't let people manage raw pointers without good reason (I wouldn't let someone use unsafe rust without good reason either)
I think there's enough data that even the best human programmers and lifetime checkers aren't able to track lifetimes over a 10 million line code base.
That's why we're stuck with lifetimes as function contracts.
> Why on earth would you try to rewrite python CRUD apps in Rust?
Because Rust has a lot of incredibly helpful features that make bigger systems far less of a pain to maintain. I work in Java/Kotlin, and my entirely gut-based estimate is that 66% of problems wouldn't happen in Rust.
My big favorites are:
- Sane, well-defined, enforced, opt-out error handling
- Sane, well-defined, enforced, opt-out handling of "missing" values
- Exhaustive switching
- WITH usable ADTs (enums) for encoding valid state
None of the web languages I know have all of that.
But even Java is pretty much there: checked exceptions are available (and imo superior), but Optional is also an.. option. Kotlin/scala can handle nulls, but java can also statically analyse every usage with annotations.
switch expressions are exhaustive in java over enums and sealed ADTs. (Rust has a misnomer, their enums are ADTs, java has both real enums and ADTs now).
This is an interesting read, because I read it as "I would have done all these things which would have kept the language more pure to my vision but less accessible".
I also found this bit interesting: "It's easier to work with than C++, but that's fairly faint praise".
I see this kind of thing in my own personal projects all the time. I'm thinking "oh it would be really cool if I built X" when in reality most of the time users just want really simple stuff.
Being easier to work in than C++ might be faint praise, but it's probably the biggest draw of Rust for me. I don't want to touch C++ with a 10ft pole, but I love using Rust.
Note that the quote is specifically about parsing Rust syntax, it’s not about the language in general (which is a lot easier and safer to work in than C++).
I'm not sure it would be less accessible. I think the trade-offs would lean towards simplicity, and perhaps less familiarity. But with the right mental models, I think this could enhance accessibility (less "magic", or strange edge-cases). For example, see the discussion on not minding if users have to write something in a more verbose way if it preserves the language's principles. This type of trade-off is something that lowers the barrier to entry for beginners, who don't mind writing things out the long way.
I'm using accessible in a very general sense. The sense of "how niche would this be". My impression is that Rust would have been a very niche language if Graydon was the BDFL.
It sounds like the Rust He Wanted has a lot of thematic similarities to Elm. Interestingly, Elm has a BDFL, and a development process that reflects that. And he’s right - there are a lot of people who really don’t like that!
Overall, a really interesting article. Though I like today’s Rust, I do think I would prefer the trade-offs made by the alt-Rust outlined here.
Perhaps it’s just my own personal preference, but I think there is a strong bias in users towards what they are already familiar with, and it’s hard to break away from those without a BDFl or similar position of authority who can impose their vision.
The language creator has preferred to work on the development of the language largely in private, for the last couple of years. He’s explained why he’s chosen to do this and why he thinks it is beneficial. But, understandably, some people haven’t take too well to the lack of updates.
I think whether you view that as benevolent or not (I would, because I agree with lots of de facto development standards being detrimental to language quality, but I understand that that is my opinion and not hard fact) is in the eye of the beholder.
One thing I want to add wrt. "Cross-crate inlining and monomorphization. I wanted crates to allow inlining inside but present stable entrypoints to the outside." [..] is that it was in general well desired AFIK by most developers but so far out of scope that you probably would need to had the resources rust has today _before_ the 1.0 release to get it done right. At lest with the state CS research had been at during that time. By now due to various reasons (e.g. swift) there is much more research in that direction already done hence a new language has it much easier.
The problem is that it's not that simple to make it work nicely you need to design you language around such constraints. Trying to do it the other way around is even harder.
For example rust in it's current design relies quite a bit on the combo of: generics monomorphisation + inlining + dead code elimination. But this reliance doesn't play well with an stable ABI. Similar features like generic associated types and similar do not make the story easier. Like an rust stable ABI likely would only support `dyn Trait` and no generics or "on the fly" provide a `dyn` variant for generic method where possible. But even that isn't really good enough for a lot of use cases in a lot of different ways. Additionally a ton of important rust things are not `dyn` compatible at all.
Even some of the "easy" to solve things aren't that easy for non-technical reasons. E.g. a lot (but not all) of `impl Into<T>`, `impl AsRef<T>`, `impl Borrow<T>` etc. cases are best handled for a stable ABI context by aplying the single function of the trait (`.into()`,`.as_ref()`, etc.) _before_ calling the function and only having an ABI stable version for that. The side of which traits qualify for this is easy (you annotated the traits) the "when not apply it even if it seems valid" part isn't that easy not for technical reasons but for communication/documentation/avoiding unexpected outcomes reasons.
> Tail calls. I actually wanted them! I think they're great. And I got argued into not having them because the project in general got argued into the position of "compete to win with C++ on performance" and so I wound up writing a sad post rejecting them
I don't understand the subtleties here: Is tail calls an optimization the compiler can do irrespective of the language? Or is there something that prevents this and requires the compiler to use stack here? Is there any visible effect to the programmer from supporting tail call or not, other than performance and stack depth?
How does not supporting them compete with C++ performance?
It’s about stack size. Programs that rely on tail calls (in particular recursive ones) may cause the stack to overflow when the language implementation doesn’t actually support tail calls, for example when using tail calls to recursively process a list that is larger than (some constant fraction of) the stack. With an infinite stack, it would just be an optimization, but in practice the stack is finite (and significantly smaller than the heap), and thus doesn’t lend itself to such a programming style when the implementation doesn’t support tail call optimization.
What I actually meant to ask is: given what you said, is supporting it then even a language feature, and not a compiler optimization instead?
If rust says they don't support it, does it mean they don't even allow the compiler to do it?
Obviously the syntax of the language itself already supports calling the function itself, and whether tail call optimization is supported or not doesn't affect the visible result afaik (except in case of stack overflows / performance)
It’s a language feature in the sense that it determines which programs are viable in the language. This is similar to garbage collection. In principle, you never have to explicitly free memory. Given infinite memory, garbage collection would only be an optimization. But since memory is finite, it becomes a language feature. Without garbage collection, programs that don’t free their unused memory will typically run out of memory sooner or later, similarly to how a tail-calling program will run out of stack space if the language doesn’t support it.
I suppose a difference of speed turns into a difference of ability at some point. If you can't rely on tail calls, you will have to manually trampoline the calls, just in case the optimization didn't kick in and your program dies.
Or like xmtp or mail or whatever, you generally write your client assuming you are going to talk to any spec compliment server, which can stop you from using some extensions if you aren't sure they will be supported.
Its not that the compiler cannot do it, it is that you cannot be sure the compiler will.
Some constructs cannot be optimized for tail recursion. Languages with tail calls have guidelines of how to must write your code to ensure the tail call happens - often a seemingly small code change is the difference between tail call optimization happening or blowing up the stack.
Second, in at least some cases where the optimizer can apply tail calls you need to be guaranteed it will happen. Nothing stops a C++ optimizer from applying tail call optimization (I don't know if any do, but it is allowed in some cases), but the language doesn't require it, so even if your optimizer supports it you can never know that it happens - and more importantly you cannot be sure that after changing the code or upgrading your compiler you will still get it. Thus even if your optimizer supports tail code optimization you dare not do deep recursion.
If your language doesn't have support for tail calls you cannot do deep recursion with confidence. If your language does, then you can do deep recursion so long as you follow the rules of the language. (whatever those are - I'm not up on the latest research here, so I don't know the state of modern tail recursive languages are.)
I thought on 64 bit systems the stack could basically be infinite for all practical purposes. Is this only a problem on more limited (i.e. <64bit) systems or am I misremembering when I last learned about stacks 10+ years ago?
It is always a problem, in the same way that holding on to huge objects is always a problem. Let's think about this in terms of a memory map, so ignoring limits of physical memory. On 64bit systems we actually have 48bits of usable address space, so that's 256TB. Now, the OS is going to need some workspace, but I think we can ignore that for this argument. So if you're only going to run 4 threads then each one could have a huge stack of 32TB, but if we wanted to run a huge server with a thread per connection then maybe we can only have 256MB per thread.
Now, 256MB is pretty huge, but we probably want way more memory kept for heap storage and similar things (because managing lifetimes purely on the stack will be hard and might require a lot of copying of data), so it will be less than that. Now, you may ask, "Why not start with small stacks and make them bigger as needed?" It's a good question, but since our stacks are contiguous areas of memory and we don't know what's in them, we still have to space them out in our memory map allowing for as much growth as is needed.
We might solve some of these problems by introducing segmented stacks, but this is one of those problems that crosses so many language, runtime, and OS boundaries that it's been hard to do in the general case, and it feels like gently nudging people towards writing code that can be tail-optimised is easier, just as it's easier to push people to use async than it is to provide systems and APIs that would allow for blocking code and a huge number of threads.
Thanks, I didn't realise the usable address space was so "small", those numbers are well within reasonable usage so that's definitely too limiting to just say it's infinite and not worry about it.
Apart from address space, there’s also simply the question of wasted resources, because in tail-call-optimizable programs, that stack space is by definition not strictly needed (otherwise it couldn’t be optimized away), so you might unnecessarily be wasting many GBs of memory. Lastly, TCO can improve cache locality and thus potentially speed up the program.
Yes and no. In theory it would be totally fine to "allocate" a stack that was exobytes in size (or whatever extremely large number). Thanks to virtual memory no one really cares... if that memory isn't actually being used. But as your stack grows larger and larger the OS has to find pages of physical memory to assign to that allocation, and you're simply going to run out of memory. A high end home PC or average server is only going to have 64-128 GB of RAM, a very high end server might have a few TB. Of course when you start to run out of physical memory the OS will start paging (dumping data from RAM to disk to free up memory), but that will kill the performance of your app even with high end SSD's. If you keep chugging along with your stack growth you'll probably hit OS limits on page file size next, if not you'll eventually run out of disk space.
That's why we write programs that rely on tail calls only for languages that have TCO in their specification. Where TCO is idiomatic (e.g. Elixir) implementations not supporting it are impossible because they will be able to run only the simplest programs.
The problem is adding TCO to a language that was born without it. It won't break existing programs but you'll be able to run a program using TCO only with a specific compiler or interpreter from a given version. It could catch up if the main implementation of the language does it, but often we care about compatibility with the alternative implementations. Think about Ruby and Python, MRI and CPython and all the other implementations of the language. If I'm not wrong, MRI added TCO under a runtime configuration variable but JRuby doesn't support it.
Tail call optimisation means no work can be done after the call and before the return. You can't deallocate stack, call destructors, convert types, rethrow exceptions.
Therefore _yes_ the compiler can always do it, but it may involve patching the called function to do some work. Splicing code into it and/or changing the calling convention. That's difficult to do for unknown caller/caller pairs, e.g. function pointers or separate compilation to machine code without enough metadata to patch it later. It runs a bit close to "sufficiently smart" compiler which usually means doable in theory but unlikely in practice.
If you pick global designs to make them easier - probably most notably having the callee clean up the stack frame from the caller - and that makes other things slower, then you've given the competition an edge.
Clang now has tail call annotations for C++, which works mostly because the compiler can reject them in the cases where it hasn't implemented the lowering. Rust can probably have it in the same approximate circumstances as C++.
Interesting that tail calls are mentioned and implied to never come to Rust. I agree with your assessment and actually there is a RFC[1] in the works to support them. (Note that while I'm the author, this RFC is definitely a community effort.)
I can't imagine Rust being even remotely viable without having generics or using LLVM to target a wide variety of platforms with sufficiently good codegen quality.
> The priorities I had while working on the language are broadly not the revealed priorities of the community that's developed around the language in the years since
Isn't that a matter of self selection? A language which developed around those priorities would have a community sharing those priorities now.
The point is if that community would be as large as the current one, larger, smaller.
The fact that the community that formed trended in that way can be taken as an indication that those directions attracted more people than the original vision. Note how often Graydon was basically outvoted on design decisions. It would have been conceivable for the project to attract mostly people that fully shared Graydon’s vision, but that’s apparently not what happened.
It's not really mentioned in the post, but one thing Rust had since fairly early on was a use case, it was used to build a browser engine. As I remember it from the time, the feedback from Servo development tended to pull Rust in a more performance-oriented direction.
Interestingly like Graydon suggests this and 'tis something D has, you can only have `ref` for function parameters. This is something the users sometimes complain about, but I guess it has positives.
I think Rust is somewhat cumbersome as a language, but I also think the right tradeoffs were made. There are far fewer and far worse competitors in the high-performance space than in the general purpose space.
I know we haven’t gotten to that point yet, but I’m curious to see if in my lifetime we get a successor to Rust that fixes a lot of issues people have with the language now.
Over the evolution of Rust, I've been increasingly despairing about many of the things Graydon here dislikes. I assumed the present "syntactical insanity" was, somehow, intended; it seems, really, it wasn't.
I find Rust basically unusable -- at the level of abstraction I want to write code, basic definitions break line limits.
Rust seems to be a repetition of C++'s mistake: a language which conspires you to pretend it's another. There are now nearly as many Rusts as C++s.
If I return to any domains where Rust would be relevant, I'd probably now opt for Zig or equivalent.
Can you give an example where Rust is unusable?
I have been getting into Rust recently and it feels very natural after getting accustomed to dealing with `Result` and `Option`.
And what do you mean about "a langauge which conspires you to pretend it's another"?
What is it that gives you problems? The first example mainly uses a method chaining style of programming which might throw you off?
Or maybe the use of lambdas?
The first example seems quite readable to me. Also no heavy usage of generics or heavy type sorcery which would be more confusing.
The second example also isn't visually assaulting. The struct definitions have macro annotations but doing this in any kind of language would lead to more verbose code.
I think the main problem might be if you are familiar with functional languages which lean heavier on data flow and lambdas.
I've included just a light rephrasing below, of the first part of the first link -- but with a better design we could radically reduce it much more. Nevertheless, I find the below less of a visual assault,
use rustpython_vm.. builtins.PyDictRef, py_compile, py_serde, scope.Scope
use rustpython_vm.. InitParameter, Interpreter, PySettings, VirtualMachine
use rustpython_vm.pyobject.. ItemProtocol, PyObjectRef, PyResult
use logic.. ProgramError, ProgramResult
setup_scope :: (vm: ref VirtualMachine) -> PyDictRef =
let code = vm.new_code_object(run py_compile(
file = "stdlib/rumblelib.py",
module_name = "rumblelib"
))
let attrs = vm.ctx.new_dict()
let run : () -> PyResult[None] = fn
return? attrs.set_item("__name__", vm.ctx.new_str(own "<robot>"), vm)
return? vm.run_code_obj(code, Scope.with_builtins(PyNone, clone attrs, vm))
let sys_modules: PyDictRef =
v unwrap vm.get_attribute(vm.sys_module.clone(), "modules")
.downcast()
.ok()
.expect("sys.modules should be dict")
return? sys_modules.set_item("rumblelib", clone attrs as object, vm)
return ok(None)
vm.unwrap_pyresult(run)
return attrs
py_to_serde(type T) ::
(py: ref PyObjectRef, vm: ref VirtualMachine) -> ProgramResult[T]
let val = return? py_serde.serialize(vm, py, serde_json.value.Serializer)
let out = return? serde_json.from_value(val)
return ok(out)
The `return?` idea of yours is nice as the `?` operator at the end might be overlooked but is a significant control flow operation.
Some of your potential syntax changes seem less readable but they might look nicer to you such as changing `attrs.clone().into_object()` into `clone attrs as object`. You probably come from a python background I would assume but the rust version is easier to parse and understand but maybe to you not visually appealing.
Having an explicit return argument I could understand. I found this weird in the beginning but this is something I got accustomed.
Not sure why square brackets should be favored instead of angle brackets.
The imports in the Rust version are also very explicit and fine I think.
Replacing `()` with `None` seems silly, same with replacing `Ok` with `ok`.
Maybe the `py_to_serde` arguments of the function definition spanning 4 lines could be considered unastethically pleasing. This is something I noticed quite a lot with Rust code. Fmt also seems to break to multiple lines quite fastly when writing function or method definitions.
I find `<>` are visually distracting and take up far too visual space for their importance. `F[X]` is less arresting than `F<X>`, the thin vertical lines melt-away easier.
I replaced () with `None` (& with ref, ! with run, etc.) because I want code to read like english (ie., literate) all other things being equal. I dont find `()` "pays the cost" of illegibility by syntactical convenience.
Changing "operator-like methods" to operator words would have a radical impact on the parsing precedence and "overall look" of the lang. ... so `clone ...` would be more readable if those changes were made. The advantage of many keyword primitives is that you dont keep reusing the same syntax for everything... syntax is there to support expression. I think more is better.
`Ok()` to `ok()` was more a broad notational philosophy of basic type constructors being unobtrusive.
Scala has gone from C-like (v2) to python-like (v3) syntaxes --- and I think the ML-ish whitespace, python-ish "beat poetry" approach is mostly just better.
I buy some arguments that symbolic redundancy can help (ie., both indent and use symbols)... but Rust's syntax philosophy is clearly to "keep adding symbols", and I dislike it.
C uses symbols, but I do think C is quite beautiful mostly -- because it's so simple. Rust is creaking under the weight of its size and symbolic choices
I would prefer not wanting to read code like english. You most certainly don't want o replace common math operators with plus/minus
`c = a + b` is preferrable over `c is a plus b`. The second one is harder to parse and understand.
When writing code you are pattern matching and certain special symbols such as `()` over `None` and `attrs.clone().into_object()` over `clone attrs as object` are just faster to detect at an instance.
Using more english prose becomes a word salad that is harder to parse than using special symbols which convey meaning. Certainly more familiarity with a programming language and it's syntax elements will help you your pattern matching mechanism and allows you to understand code faster. In my opinion Smalltalk gets it quite well in this regard.
I also would rather prefer lisp language syntax, or apl.
Rust is a low-level programming language meant to control everything about the code’s execution. It will by almost definition, be complex.
You seem to want contradictory things — if you don’t need that level of control just use any of the litany of high level languages with nice syntaxes. I don’t think we can eat this cake anytime.
The major innovation in this space is to make the compiler an interpreter which can evaluate the language at compile time (and emits code for runtime where it cannot).
This makes brining in the "full power of dynamic languages" almost trivial and extremely performant.
Delete half of rust's symbols, get rid of its macro system, rewrite it until lifetimes are inferred /or/ allocators are explicitly chosen, etc. etc.
I don't want to feel visually assaulted when writing the type signature of the sort of function common in a dynamic language. Here `mypy` is also treasonously guilty.
Consider, eg., zigs "function which returns a type at compile-time" is an example of where blindingly-obvious syntax retains its blindingly-obviousness because of compile-time eval... ie., we dont need a "second syntax" to program the compiler.
This "two syntaxes, one for the runtime and one for the compiler" approach -- has swamped Rust as it aims for greater expressiveness. Not least, because it has a third syntax: one for unsafe.
You quickly gets into the undecidable category once you to down that road, which might be fine if you either don’t care about safety, or have runtime checks for that, but none of those were an option for rust (or at least an option that would have made it remotely interesting).
It's undecidable in the general case, but it becomes doable in non-Turing-complete languages that are still expressive enough to be useful for many practical cases. (These languages can express unrestricted recursion as an I/O-like effect that's only available outside the language proper, as part of compiling to a binary.)
Then you are not the target audience of a low-level language that supposed to run without a runtime, but still safely. Don’t try to change the tool, when you could just choose a more fitting one.
> This "two syntaxes, one for the runtime and one for the compiler" approach -- has swamped Rust as it aims for greater expressiveness. Not least, because it has a third syntax: one for unsafe.
What are you referencing, concretely? For example, "unsafe" doesn't add or remove syntax.
Split the syntax into whether it 1) emits no code (compiler-facing); 2) emits weakly-related code (eg., macros, etc.); 3) emits code close to what's written abstractly (eg., value operations); 4) emits code close to what the machine needs (eg., unsafe code) -- etc.
And you'll find about "4 languages" all mixed together.
I still have no idea what you're talking about. Can you give an example?
Most "syntax" in AOT, statically typed languages does not directly generate machine code, but it does directly impact what machine code is generated. So there's not a clear distinction in practice.
For example, a lot of the syntax is used to control method selection and verification - that's not a "different" syntax or language by most folks' definitions.
Compare this with lisp, where there's one syntax -- compiler operations are "compile-time" interpretations of the very same syntax.
Or below, let's invent a language where there's (in my sense) "one syntax for everything",
Eg., consider something like,
const SimpleTrait = trait with:
val name
def GenericApiTrait(type T) =
return new trait with:
def response : self -> str
const MyAPI = GenericApiTrait(SimpleTrait(new class)) with:
val name = "World"
def response : self -> str = f"Hello $name!"
# later on, in the app,
def calc(x, y) : int, int -> int =
return x + y
# ie., the **same** syntax as that earlier which targets compile-time
Here you can see that code with ordinary run-time semantics is used for the compile-time operations of generating a trait and specifying that a class implements that trait (ie., we call the trait-defining function on the class).
Whereas in Rust, the syntax for returning values and "quantifying over types" is radically different.
This is the historical approach (C++, C# .. almost all langs) -- but not one modern innovative langs follow.
I think with as much desire for novelty rust has, cramming it all into its "compile-time syntax" has hobbled it.
A simple uniform language for both c-time and r-time could be used
Ah ok, I see what you're saying. It's an interesting idea.
One of the reasons that something like unified syntax is not preferred is because there are some important distinctions between runtime and comptime, and a lisp-like syntax doesn't translate well because lisp doesn't have a meaningful distinction between the two.
For example, it's desirable for some folks to have the "comptime" work (declaring types, imports, function signatures, etc) to have a purely declarative syntax. And languages like Rust use declarative syntax to express things about types, interfaces, and their constraints. Contrast that with the logic of the program which is more convenient for a programmer to express in the imperative style.
> The major innovation in this space is to make the compiler an interpreter which can evaluate the language at compile time (and emits code for runtime where it cannot).
Show us an extant language that does this, then.
Partial Evaluation (automatic) was a very unsolved problem, last I looked.
Yeah... Rust was first intended to be a high-level language where one could create an entire browser engine without sacrificing a lot of performance.
The fact that it is a law-level language best fit for system programing came much later. Late enough that there are people here and there still trying to use it for the original use-cases.
521 comments
[ 4.6 ms ] story [ 402 ms ] threadI know that the main point is about governance and how having a BDFL would have led to a completely different language but I really would have preferred the Graydon-BDFL-Rust to what we have today.
Very interesting article, worth a read.
> Traits. I generally don't like traits / typeclasses. To my eyes they're too type-directed, too global, too much like programming and debugging a distributed set of invisible inference rules, and produce too much coupling between libraries in terms of what is or isn't allowed to maintain coherence.
Very much this!
> I wanted (and got part way into building) a first class module system in the ML tradition. Many team members objected because these systems are more verbose, often painfully so, and I lost the argument. But I still don't like the result, and I would probably have backed the experiment out "if I'd been BDFL".
I seen this the ML first class module system mentioned in a few different contexts and want to pick it up at some point.
Being a relative rust noob compared to other languages, I always felt traits were a super power when compared to eg interfaces in Java/C++ and flexible but with useful constraints compared to the structural typing nature of Typescript interfaces
Which types grow new trait methods is dependent on trait implementations, which can be generic and apply to an unbounded number of types, based on complex matching rules, and figuring out requires reading every impl of that trait to see if any match a given type, or asking the compiler/IDE. I want to explore languages which explicitly select a trait implementation at the call site (like a Heap<int, int_greater> type which uses int_greater::cmp() to implement a min/max heap), or naming the trait (but not picking an impl) at the trait method call site (like Write::write_all(stream, "hello world") or (stream as Write).write_all("hello world")). I think Zig takes a similar direction.
For example, in the real world, there are multiple ways to order strings (called collation), but since there can be only one implementation of (String, Ord) type-trait pair, one ordering is canonical. That may be bearable, but having canonical (String, Hash) implementation is not. What if you want to use (say) faster CityHash instead of canonical MurmurHash? So Rust resorts to things like BuildHasher, to get back multiple implementations.
Because it is global/anti-modular, it interferes with separate compilation, and it is one of reasons why Rust is slow to compile.
Then why would one use trait instead of module? Since there can be multiple implementations in module, you need to specify. So module is more verbose. In my (and Graydon's) opinion, a bit more verbosity is worth it for modularity, but many people disagreed.
This is another theme: Graydon is okay with verbosity, boilerplate, and being bureaucratic. In my (and Graydon's) opinion, programming is work that is secretarial, not artistic, so it is unimportant whether code is ugly or not. You may think current Rust is ugly, but no, it is the way it is because lots of people really cared about Rust code being pretty. If Graydon was a BDFL, Rust would be even more ugly, and in my opinion, as a result, would be a better programming language.
Recently I've seen a lot of programmers I respect consider them a weaker or failed alternative to typeclasses, basically a dead-end. And I had been reluctantly coming to the conclusion that I must be wrong in some way I'm not able to fully perceive yet. Seeing such a notable PL designer come out on their side makes me feel less like a fool.
Canonical instances goes against modularity but something like modular implicits for ML-family languages could improve the terse was a lot to something closer to Haskell levels. You still miss out on the other advantage rust has: the dot operator is great because it allows identifiers to contain less context (they don’t need to have long global names or be hidden away in some tree of modules: you can have .map mean different things for different types) and gives a massive hint to autocomplete for which things to suggest (I don’t know how you even signal to autocomplete in a language like ML that you would like a function to operate on the following value so please suggest things with the right type).
But I guess enough syntactic sugar might make things palatable.
Selecting and enforcing usage of a subset of C++ is a way to deal with that mess which companies frequently use.
Sure, but that's why Carbon and Cppfront are a thing now. These two actively remove stuff from C++.
We'll probably see this happen in the Rust community as well once the newer Crab language becomes established. Then we'll have to rewrite everything in Crab, but it will probably be a lot simpler than the Rust rewrite.
Besides, the C++ committee does, in fact, remove stuff from the language to clean the mess, just not without due care.
I feel if the Haskell committee could simply pick one of the alternative preludes, and gather some packages into a new standard library, and announce it all as a Haskell 2.0 or Haskell prime or whatever it could really reinvigorate the community. But what committee would be motivated to do such a thing, bearing the responsibility if it went wrong?
I would really like to be able to specify what prelude I want at the .cabal file. AFAIK, that's one change that wouldn't break anything.
(Anyway, Haskell is one case of a language asking for a fork. Maybe it takes the form of Idris or some other derived language having some sudden growth instead, but you are right, the committee currently isn't as fast as the community.)
The bigger problem is that strings are broken: by default they are linked lists of Characters. You can use better strings (like Data.Text) and you even get to use them as literals in your source, but the language ecosystem of libraries mostly assumes that you are using the default strings. And converting back and forth is annoying.
Because of all the existing libraries, it's harder to route around [Char] by yourself.
Rust as an alternative to C++ does, for me, involve all of the weird magical nonsense that you kind of need to get any of this working. The extreme use of generics to build out DSLs to get things working. General libraries being very hard to write, but still possible, to get alright ergonomics for usage itself. And yeah... the zero-cost abstraction thing.
I think Graydon-Rust would have also been very interesting, but it sounds unappealing to me, person who wants "C++ but nicer".
But to his point... saying "you could have much faster compile times" is very tempting! Just, especially when it's messing around with Rust "for fun", the ergonomics sound pretty unfun.
So if you want to try something like that, just look at Go :)
StandardML uses a far superior Hindley-Milner type system. It has pattern matching. It has Option types for good error handling. It doesn't have bad features (for GC'd languages) like direct pointers and slices. It has immutability by default. CML even offers a better take on channels too. Modules keep interfaces more standardized (superior for big projects IMO). And to top it all off, SML is easier to learn than Go. It's also as fast as Go (despite the compilers being a side project).
Go's only advantage is more extensive libraries, but that would be fixable in SML with just a fraction of the money Google spent on Go.
But it also turns out the very same isn't as neurotically attached to his 'likes' as most of us are:
> The Rust I Wanted probably had no future, or at least not one anywhere near as good as The Rust We Got. The fact that there was any path that achieved the level of success the language has seen so far is frankly miraculous. Don't jinx it by imagining I would have done any better!
He understands that his likes are just contingent facts about a single mammal, not truths. This is a hard-won understanding. Most people never reach it.
Yep. That just goes to show that you shouldn’t put individuals on pedestals as if they are superheroes. More things than we usually think are in fact team efforts.
> I know that the main point is about governance and how having a BDFL would have led to a completely different language
That isn’t the only main point.
> > The Rust I Wanted probably had no future, or at least not one anywhere near as good as The Rust We Got.
If you would be fine with a niche language then that “Rust” would have worked for you. But if you also wanted a language with wide industry backin etc.—maybe not so much.
That said, I still hate async with a passion, it makes the language more complex and not very elegant (i.e. function coloring). And now that I know how it works behind the scene (thanks to Jon Gjengset [1]), it feels so complicated and hacky, a mediocre very high level concept that someone managed to implement as a zero-cost abstraction. Impressive, but still a bad idea.
I'm sure the pro of having a BDFL instead of a committee is being able to follow a singular vision, instead of trying to appease members by adding the fad du jour which might stray a little too far from the original vision. Too many chefs in the kitchen and all.
1: https://www.youtube.com/watch?v=ThjvMReOXYM
I stopped paying attention to the language around 2012-2013 or so when the direction clearly steered towards the latter.
You may want to check https://austral-lang.org/ out.
OCaml is / was short for Objective Caml. Hence the name used to have an apostrophe. At some point that was dropped.
It's even hard real time capable, which makes it potentially viable in scenarios like games where GC traditionally hasn't been.
Rust's async doesn't have the limitation author described – you can spawn and block both (Tokio has some limits there, but that's Tokyo's choice, not language limitation), making "color" largely irrelevant.
The second point was that both should have identical syntax, which Rust deliberately chose not to, because from Rusts perspective that would be too much implicit magic.
Async without function coloring means being able to call `async fn add(a, b: usize) -> usize` from anywhere and having an usize back, whether you're calling from a regular or async function. If you need slightly different logic because of async, you got function coloring.
In fact, a hypothetical async without coloring functionality would not even need the `async` keyword at all, as all functions would be effectively the same, so you could choose to call one asynchronously or not with no particular ceremony. Is this even possible to do without compiler magic?
So the two camps I've seen on solving it is either
a) blackbox it and set some rules for implementers, inevitably leaving to some hidden and very nasty bugs on the user end or bottlnecks for advanced users, or
b) give implementers the full tools to setup, launch, and synchronize themselves, which may end up being slower than a single threaded solution if the user isn't adept already with parallel programming.
You inevitably always have bits of a) creep in, even for the most explicit solutions
The insight that function coloring gives you is terribly important. Roman Elizarov (of Jetbrains, author of Kotlin and lead on Kotlin Coroutines) agrees too: https://elizarov.medium.com/how-do-you-color-your-functions-...
async functions without coloring means that the only warning you'll ever get that `calculate2Plus2()` actually ends up running a distributed BigQuery and writing the end result to disk, printing it to stdout() and parsing that result to give it back to you is... hopefully, documentation is up to date and you read it?
Async function coloring is not a problem. Async function coloring is a solution to "software developers are awful and will do awful things without any warning". If `calculate2Plus2` did not exhibit the write to disk behaviour in v1.0.0, but bumping to v1.0.1 does, I'd really want a warning that it does at least, and ideally a compiler error. The proper solution to function coloring is to have a pleasant API to interop between both worlds so that, at worst it's just a dozen characters more to say "yep, I really want to block here".
So the whole sync/async distinction doesn't really make much sense outside the context of single threaded UI toolkits, where indeed you have to read the docs anyway because doing some very CPU intensive work on the UI thread will block it even if no "blocking" APIs are ever invoked (and what is or is not blocking is somewhat arbitrary anyway in many cases).
Alternately, an effect system can communicate "touches disk", can make functions which are polymorphic to effects (e.g. map touches the disk if the mapping function touches the disk), and can distinguish different effects, unlike async/sync being anything-or-nothing. IMO async is more of an implementation detail (as "may suspend/yield" is not a very precise or interesting effect), whereas effects would communicate the properties that ohgodplsno wants to know.
https://dev.epicgames.com/documentation/en-us/uefn/specifier...
converges/diverges, computes, varies, reads, writes, transacts, no_rollback.
Blocking has meaning in a lot more contexts, and being a consultant on JVM related topics, you should know that: it's the entire purpose of Project Loom, and Loom doesn't entirely get rid of colour coding either explicitly for that purpose. Loom wasn't made because the guys at Oracle have a deep love for JavaFX, but rather takes into account the server world, where you really want to know that you're going into another context, another computer, etc. The only time where the existence of async doesn't make sense is if your entire language and ecosystem expects everything to already be distributed. In which case, you've just switched the default color to async.
Finally, you chose to read async as the current JS/C# abomination implementation, but most of the sensible languages have implemented it as an effect: Kotlin has suspend funs, they don't return a Promise, but they tell you two things: they're going to touch something like the disk or the network, and if you really want to have them in a non-suspend context, you can either get a Deferred<T> out of them (and find another thread to run it on, and handle synchronization yourself), or run them on the current thread (and block everything).
Kotlin suspend funs do not tell you they're about to touch network disk, that's the reason they use "suspend" and not "async". Suspend funs can also use generators (with yield) in which case no I/O is happening but they are nonetheless suspending.
So this is why blocking as a concept isn't a great one, IMO. The Kotlin designers were right to not use the word in their implementation of coroutines. Where Loom discusses blocking a bit, it's not defined as being about blocking, it's about being able to scale up threads to way beyond what was previously possible. It just so happens that the primary reason you'd want to do that is if you have lots of threads that spend lots of time waiting for things, but that doesn't automatically require network or disk access. For example you can use threads that spend all their time in Thread.sleep if you were writing an agent simulation.
Wasn’t basically all of the JDK’s file APIs rewritten to io_uring-like calls to support Loom? I have thought that IO was definitely something that Loom handled.
You're conflating too many orthogonal things.
It's not merely because of the performance of touches disk/network that async was used for those cases, it's because that waiting is not because you're held up by the language doing calculations. That isn't the case with a function like you describe.
Marking functions async when they aren't yielding just to signal that they might be slow is a bizzare idea. That's not what async is and it doesn't bring any real benefit, it's just abusing the notion (and limiting the contexts where you can run those functions). You'll still be using libraries which wont follow this strange idea, and you should know their performance characteristics.
Blocking code exists in all major languages, including JS. In a single thread context, having something "async" wont help you at all, if it calls anything blocking, which can be something as common as JSON.parse
>If it suddenly expects a disk write and I'm running on a ROM, I'm going to have a hard time. If it suddenly tries to contact google.com and I'm on an airgapped machine, I'm going to have a hard time.
All of those have nothing to do with async, and what async is created and used for.
What you want is something like Haskell's IO "tainting", a (side) effects system, or something to that (no pun intended) effect.
As a design choice of OCaml’s eio - an async runtime, you must pass around a dependency to all your IO functions. With the “net” object passed visible in my function type signature, I can tell some network work may occur. The first benefit is we get synchronous Go-like code.
In case of java there really is no compiler magic even, “just” runtime magic.
With Rust's async you have to worry about a different worker thread picking up the work after a context switch, which makes things complicated. Not so with Java.
Its a lovely design though.
https://kristoff.it/blog/zig-colorblind-async-await/
To my mind it's very much a balancing act between "low power to the developer, high power to the language" and "high power to the developer, low power to the language" all up and down the stack from software / hardware to "consumer / framework".
TL;DR - he says that "doParallel" and "doConcurrently" are separate operations with distinct semantics that designers of programs must care about and that conflating the two (especially the common "doConcurrently-and-often-but-not-always-in-parallel") is one of the most common causes of bugs in programs that need to make progress in multiple threads of execution.
To schedule tasks properly, you need to know who is waiting on them as early as possible. With a promise, you only know when you get to the "wait()" call, which is too late.
The correct solution is called structured concurrency.
The issue is that you want to be able to save the entire thread stack cheaply so that you can switch to another task at an async yield point, and then go back to your previous stack frame. You want to do this without spawning system threads.
Say you have an f -> g -> h call stack that blocks on IO. If all the functions are in fact async state machines (or some other kind of callbacks) your thread stack will look like this:
Executor.loop() -> Executor.run(h) -> h.await()
That is, your functions are really objects that are queued up and taken as needed by the executor. If h has a yield point, you can put it back on the queue, go back to the loop and pick up some other work. Later you can do the above again and await h to again up to the next yield point.
Now consider the case where the middle function g is NOT async. In that case the call from f to g will be a normal function call that gets put on your call stack. In a language where this is allowed, you'll still have to wait or execute h and you'll get a call stack that looks like,
Executor.loop() -> Executor.run(f) -> f.await() -> g() -> h.awaitUntilCompletion()
(where our awaitUntilCompletion is minimally just calling h.await in a loop until h is finished). At this point, you are stuck: you can't yield from g() because it is a normal function with a normal stack frame, so your thread has to wait till g() finishes before it can be used for anything else. You are basically blocking a thread on h. At this point you might spawn a new thread to keep your thread pool count up (which is expensive and memory hungry) or just accept the forced synchronous blocking (which reduces throughput).
If this happens over and over again, you either end up running out of memory or end up running code in a synchronous fashion but with worse performance (because of all the state machines). This is why functions are usually coloured, so that you don't get yourself in this accursed state by accident. AFAIK the alternative is to allocate all of your call stacks in the heap so that you can switch them in and out of kernel threads, which is what fibers are. This requires a complicated runtime with a scheduler such as in Loom,
https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html
https://github.com/mirage/ocaml-cohttp/blob/16e991ec1f7e5f0c...
Performing these effects is similar to throwing exceptions up the callstack where whichever ancestor handles the IO work, then resumes the child with IO work done in hand.
- what the type of res is (is it a string? A buffer? Async string? Something else?)
- how this code does not block: what is happening in parallel? At which point will it block?
I would like to be excited about OCaml’s algebraic effects, but right now I don’t really get it.
- It's implementing non-blocking I/O using effect handlers. The complexity is not exposed to the library user, which is actually the whole point. If you want to dive into the concurrency library (Eio) and study how it works, that's very doable, just like any concurrency library in any other language.
There's really not that much to get–OCaml will do async I/O without having to colour functions, just like Go, Erlang/Elixir, and soon Java. It's not something sexy like monads that excite people with mysterious FP concepts, it's just a lot of hard work that went into simplifying how developers code at scale.
When reading ocaml on _text_ like from GitHub, I personally accept that I won’t need to know the type and underlying data structures just yet unless I read and have open the .mli file.
I’m excited first and foremost because we can have async code feel synchronous like Go.
How does the code run? IO runtime is defined in userland, like Rust’s Tokio. That’s the “Eio_main.run” part. Underneath, eio spins up and manages processes in a non blocking manner, even scheduling work.
Since effects are essentially resumable exceptions, this particular http request throws an effect to the runtime. The logic and this particular process pauses (okay I may be wrong here). Logic flow now continues somewhere in our runtime level (the effect handler), where all the async IO work happens. The runtime resumes the child process and returns the http result back to the child via a “delimited continuation”.
IMO, this is even worse than function coloring.
Rust has basically 2 executor libraries, tokio and async-std. It seems to me like tokio is solidifying as the executor of choice and it's only a matter of time before that design is baked into the std library.
std::async is mostly "good enough" for ordinary cases where you only want to fire-and-forget and have no control over the underlying dispatching and scheduling algorithms. If you want something tweaked towards your use-case, and hence actual high-performance executor library, std::async is not gonna cut it. And that's where the "C++ executors" proposal come in.
I think designing and evolving any living programming language is just one of the hardest problems out there.
Incredible blog post indeed, was awesome to read it.
His dislike for functional constructs didn't do Python any favour, but now that he's gone I see Python is adding the kitchen sink as well. I haven't kept up with the language since that pattern matching proposal.
If they're all terrible, then the language won't appeal to anyone and will die off naturally.
On the other hand if everyone gets to add their favorite terrible idea, there may still be enough non-terrible parts to appeal to enough people to keep momentum.
One of the most popular and succesful languages, and a major force in AI innovation?
The transition was slow (Python 2 was supported in parallel to 3 for a long period of time so there was more than enough time to migrate), relatively easy to do, and brought huge benefits to the ecosystem.
It could have been done and forgotten in 2 years if some community members had not been dicks about it.
JS devs have been transpiling for years now and it works.
Python should have released a conversion tool that could automatically convert most stuff and identify the stuff it couldn't convert. This kind of directed upgrade would have made the process much easier for developers to actually accomplish.
https://docs.python.org/3/library/2to3.html
Sure. The point is that a community can make things go poorly, with or without a BDFL.
Here's the timeline the way I think about it:
- 2008-2012 Python 3 becoming usable (byte formatting, six library)
- 2012-2016 Libraries (Django, etc) becoming compatible
- 2016-today Applications (Trac, Ansible, Chrome, etc) becoming compatible.
It took a lot of time for the ecosystem to be ready. Yes, there was a lot of stubbornness, but that's reality.
There are many things that could have been done to make the transition go smoother.
There's a lesson in there to learn. Its unfortunate that the people who need to learn it most likely won't.
(That said, JS is actually a very versatile and almost-great language, getting better all the time)
https://peps.python.org/pep-0020/
Things like "There should be one-- and preferably only one --obvious way to do it." just was such a breath of fresh air in language design.
Function coloring seems to come up a lot in these discussions, but I don't see a better way without providing a runtime. Could you propose an alternative approach to async, without sacrificing ability to write zero-overhead, high-performance bare metal systems?
They're not expressive enough to implement a monadic async library, as far as I'm aware, so they don't fulfill the role of proper HKTs in this context.
>minus `template`’s duck-typiness so you’d have to spell out what contract the container must adhere to.
Do you think it's consistent that the T in MyContainer<T> can be "duck typed" but not the MyContainer?
For an example of GATs, you can see the following:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
AIUI you need GC in order to do that. There's a reason why Rust's async/await support needs compiler magic.
(I'd like to see standard interfaces for "pluggable" garbage-collected arenas in Rust, but this will need to wait until after Allocators/Storages get fully stabilized.)
I really wish people stopped using this concept, especially in the context of Rust because the `async fn`/“regular function” split is strictly equivalent to the `try_something()`/`something()` split (the first one being failible and returning a `Result` in case of failure). `Result`s and `Option`s are coloring the stack in exactly the same way a `Future` does (and `async` is pure syntactic sugar on top of future).
So, someone may like exceptions and green threads more than `Result` and `async` (and this is a completely valid PoV, even though I personnaly like the explicitness better), but thinking `async` is somehow special is just a conceptual mistake.
BTW, in the original blog post the “red” functions were actually functions with a callback parameter, which is actually very different.
Tons of stuff in std and many other crates don't use async. A lot of stuff is harder and more code to do async. A lot of it is much harder to read.
That's the difference.
Well, there are dozens[0] of incompatible error types in std, two of them being just called `Error`[1][2].
Everytime you create a function that use more one of the different error types from std (for instance, an I/O error and a TryFrom-related error) you have to create a custom Error enum to accommodate the different kinds of errors from std…
> community agreement to do things that way
And the community agreement is e̶r̶r̶o̶r̶-̶c̶h̶a̶i̶n̶, ̶f̶a̶i̶l̶u̶r̶e̶, ̶ s̶n̶a̶f̶u̶, this_error, unless it's for a binary crate and then it's ̶a̶n̶y̶h̶o̶w̶, ̶e̶y̶r̶e̶, color-eyre.
The error ecosystem is much more fragmented than the async one (even though I think this_error is here to last, so I don't have to migrate once again…). In the async world, there's pretty much tokio (but I think it would actually be better if it interoperated better with other runtimes so alternatives could emerge)
> A lot of it is much harder to read.
Ah yes, fut.await is fundamentally harder to read than res?, right?
Overall, you vastly underestimate the friction that the `Result`-based error handling adds. I still think it's worth it, and it's been slowly getting better (did I say I loved this_error?), but it remained one of the biggest point of friction in Rust over the 8 years (time flies) I've been using it. Async is comparatively less disrupting (mostly because it only appears in the call stacks where you're doing I/O, whereas errors are ubiquitous).
[0]: https://doc.rust-lang.org/std/error/trait.Error.html?search=...
[1]: https://doc.rust-lang.org/std/io/struct.Error.html
[2]: https://doc.rust-lang.org/std/fmt/struct.Error.html
A nice talk about this: https://www.reddit.com/r/programming/comments/da141r/ron_pre...
More concretely, threaded code has nothing to say about whether or not its synchronous or asynchronous, and asynchronous code has nothing to say about whether or not it's evaluated in parallel.
That is an artificial distinction you made up. I have written many apps and desktop applications, all of which where using 3-10 threads, some of them doing "parallel" work meaning the parallel computation of divide and conquer algorithms, some were doing concurrent work.
I tried async often. I threw it away every single time, because it "infects" the entire codebase. I suspect the only reason async exists is because the Javascript event-loop is single-threaded and there are no blocking primitives in JS. If you don't have access to blocking data-handoff between threads, you can choose between callback hell or async/await.
Please take a look at the video I posted earlier.
2, 4, even 256 sockets exchanging data can be worked with concurrently on a single thread and gain performance vs blocking and waiting for the first socket to finish. There's no parallelism, since they're never actively reading/writing at the same time, but they're concurrent because they exist and operate in overlapping timeframes.
Running two independent algorithms could mimic this - run part of algo A, then part of B, then A, etc. It's not useful for performance, though. To be useful you require parallelism - you have to have the algorithms executing at the same time, using multiple threads.
On async itself - it's not perfect, but honestly, there are contexts where it makes a lot of sense. I work on a lot of non-blocking C code - the entire programs are basically epoll and timer driven. As a result, the high level coordination is just callback hell. Async is very nice in comparison.
Yes, you can spin up threads to do everything with blocking, but non-blocking I/O came around specifically because the threads add overhead and kind of suck. It's worth noting that having threads can also infect the codebase. You either have to carefully manage mutexes, or you only communicate with channels and have to worry about keeping things updated and in sync. Sometimes this works great with minimal communication between threads. However, if you have one socket per thread and the sockets are all triggering actions that mess with the same data... it's not so great.
Besides IO Completion Ports for async IO has been the fastest way to do heavy IO on Windows, and it was introduced years before JavaScript even was a thing.
And that's just one example. So hardly think JavaScript was the main driver there.
Rust Channel have the nice additional that they don't panic, and don't have all the weird Go Channel Axioms[1] that you have to wrap your head around to get things to work correctly[2].
For what it's worth, I don't think Java Virtual Threads (a.k.a. project loom) has implemented an explicit concept of channels, although I guess BlockingQueue could work as a channel.
There is a big issue with bringing CSP to languages like Java 21 or Go, that use so-called "colorless concurrency" or "stackful coroutines". CSP losses a lot of its modeling power when you combin shared mutable state and pre-emptive (or invisible) parallelism. And this is exactly what both of these languages do.
"Colored" abstractions like async/await are more cumbersome (especially in Rust), but they let you know exactly at which points of execution your stackless coroutine may suspend, and you can handle synchronization accordingly.
Those functional concepts are cumbersome, but threads ("green", "kernel", "virtual", "goroutines" or otherwise) are not the panacea their proponents claim them to be. I had to debug way too many data races in my life to consider them perfect. Rust still has a lot to improve on its async front (no async traits, async closures or async iterators and the async runtime confusion), it can at least promise me that I won't get any data races.
[1] https://dave.cheney.net/2014/03/19/channel-axioms [2] https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...
But not any other kind of race condition.
Also, data races are “safe” in java. Plus you can rely on immutable data for those parts. Parallelism will always be hard, there is no model for shared mutability that would be easy.
Being sync or async is essentially a property of the attention of the caller, not of the action itself. Is "eating a donut" a sync or async action? If I'm focusing all my attention on it, essentially putting all tasks aside (after) - then it's a synchronous action. If I'm reading a book/watching a video/walking/etc, while eating – it's an async action.
How does "func EatADonut() async {}" aka "eating a donut is an inherently async action" even make sense to people?
But consider a network request. The vast majority of the time is not spent in the CPU.
Right?
s/eat a donut/make a network request/
Is "network request" a synchronous or asynchronous activity? It depends whether your code blocks and wait's until response (or timeout) or continues executing and handling it when it comes. It's property of the "attention" of the caller, not of activity itself.
Something that pegs the CPU to 100% because it's doing intense processing isn't a good candidate for async. Similarly, some code can cause issues when written in a non-streaming fashion. Take the following example (in python):
If you spawn a bunch of async workers doing the first one concurrently, you'll OOM your system. If you spawn a bunch of async workers doing the second one concurrently, your system will be fine.In other words, "async" is a label on a box of donuts that implies (though doesn't ensure, people can of course still do bad things) that the donut won't explode if you look away from it.
Even if this is an original logic, why language is deciding for me what is considered heavy or not? What if I'm fully aware that I'm doing heavy processing and I want it to be happening in the background. What if I'm writing HFT software and every call is heavy for me. Language is not a right level of abstraction of marking "heaviness" of the code.
It really just doesn't make sense. Why stop there and not start marking functions with how many times per second they can be called? Like you can call "light" function 100 trillion times per second and OOM the system, so let's mark it "func light() async 2_times_per_second {}". It only can be called from functions that have lower per second label.
Plus, if Python cares so much about not OOM-ing the system I would start by not requiring 32-bit int to occupy 28 bytes in the first place.
Well, I'm not that deeply versed into any language's history, but I imagine any 15+ year old language had a point where they needed asyncrhonous functionality but had to write around legacy code. I don't think Javascript would have been written the way it was in the 90's if asynchronous operations were a mandatory priority.
So it's probably a lot more "this is how we hack this in without creating Python 3.0". Relatively elegant to make it an optional part of the language that you explicitly choose to delve into, instead of a core feature that would have broken thousands of sites behind the scenes if you made it "right".
>why language is deciding for me what is considered heavy or not?
because we decided decades ago that we didn't like using fork(), nor creating/destroying processes ourselves. problems that would inevitably need multiple solutions when supporting multiple platforms because these aren't language features so much as OS calls made in a language that you may or may not be writing in.
Remember at the end of the day that languages are abstractions on top of an OS. Any interesting ideas for problems involving more than a single process space that your OS makes for you is bound by what the OS's API lets you do. It's arbituary but also based on historical problems to wrangle.
So we haven't had enough problems where we need to bound a function by how much it is called. We have for how to interact with all the above OS/language problems.
Regarding stickiness of function colors - it never happens when async/await is used correctly (in the same principle as IO monad isn't sticky).
The question is how you translate mental models from real world to the code, and async/await fails here spectacularly. It's just weirdly unnatural thinking about sequencial processes. It requires tremendous amount of cognitive gymnastics just to reason about simple things.
Of course it does, read it as "beware, something blocking down the road".
If you can EatADonout without blocking, please do, but want it or not that's a different implementation, one that doesn't block and the signature it's telling you so.
We're so used to sync and having hidden blocking operations. I wonder if in an alternate universe the first languages considered the blocking/async nature of operations and then some newer languages considered hiding this information into seemingly sync functions would produce similar but opposite outrage against it.
Implementation is the same. In both cases it's the same set of CPU instructions, but async/await languages create artifical division, forcing developer to think othervise.
So, let me explian my reasoning. Code starts with a developer's mental model of a problem and behavior of the system and then translating it into the code. The more straightforward this translation, the more readable the code. Code is a second degree map of problem domain so to speak ("real" world -> mental model -> code).
Like if you want to add two values, the simplest form of code would be "add(2,2)" which is pretty straightforward. If the code forces you to do some mental gymnastics (i.e. "2 2 addOnlyEvenNumbers") - that's less straightforward, less clear and less readable.
In the same vein, if you want to execute some function ("EatADonut" or "MakeHTTPCall") – you may care or not care about blocking and waiting for results. But it's your call. So it makes sense to give you two options to run this function. Go has simplest possible solution – "eatADonut()" vs "go eatADonut()". It doesn't matter what is a "default" here – it could be "eatADonut()" (go to background) vs "sync eatADount()". What matters that "eating a donut" is just a set of instructions to the CPU, and it's up to caller to decide how you want to execute it in terms of concurrency.
Now, "async/await" approach turned this ownership of "synchronicity" around. Now function is deciding how it should be called. Mental model of "actions" now needs to be translated into "actions being async or sync for the purpose of fitting into this language concept". Which is cognitively expensive for no added benefit.
Sure you can rationalize it, and get used to it as to any other absurd design, but it still adds unneeded complexity to the code, makes it less readable and less clear.
It's actually a very different set of CPU instructions. the function EatADonut is the same set, but "async" means the kernel needs to take time out of its execution to do some action as small as accessing an open thread, or as large as "gain access into a completely different piece of hardware" before putting that set of instructions onto that different thread. Not the program, the actual scheduling process between your program and the OS you are executing on. Then it needs a way to to get that result and sync it back onto whatever thread spawned it and access the result.
It is in fact a huge action, so marking it with Async is basically a very explicit warning.
>Which is cognitively expensive for no added benefit.
On the contrary, I can't even begin to imagine the amount of compiler optimizations it saves on as well to have that be explicit in code. I'm sure Go has to do all that on the fly while a colored language gets to allocate all those potential processes before the program runs. It's only no added benefit if you dont care about performance. But to be frank, you probably don't need more than a single thread if your problem isn't bounded by perormance. Parallel programming is all about getting something done faster after all.
You have to think of it from a higher level.
Nobody knows if eatdonut is strictly async but everyone knows all IO calls are async. So something like socket.send would be async, this is obvious.
Then anything that calls socket.send would then in turn become async itself. The async sort of spreads around like a virus. Any function that uses other async functions gets polluted with the async marker.
So in this sense, you should think of it like this. If eatADonut is async it means it touches IO. It sends data somewhere else to some other thing. It also means it's not unit testable.
In a nut shell This is the intuition you should have about async:
This is largely identical to how the IO monad works in Haskell. Unless you want all your logic polluted with the IO monadic wrapper or the async marker you have to learn to segregate your functions into async functions and non-async functions.Under this intuition, EatADonut, by good design principles should NOT be async. This is what it should be:
The async marker forces you to organize your code in this way, You have to do this unless you want all your functions to be polluted by the async marker. This is the proper way to organize code Anyway, as it allows your code to be unit testable. Async calls are, in general, not unit testable because the functions are not pure. So if you look at what I did above, I segregated eatAdonut Away from IO and made part of the logic of the overall code testable via unit tests.IO calls should be very general and small functions while your other functions should be pure and unit testable. This is the overall intuition you should have about async.
Believe it or not, this specific aspect of async is actually an improvement over golang style concurrency. Not an overall improvement, it's still debatable which style is better, but I'm saying the async marker is actually a feature that other styles of implementing concurrency don't offer.
Basically, async encodes the concept of IO and function purity into the type system. It allows the type checker and You to tell which functions touch IO and which functions are pure and unit testable.
People think unit testability is some kind of marginal benefit because you can cover testing with integration tests which are basically tests that happen across IO. It's more than that. The ultimate dream of programming something as if it's a bunch of modular lego blocks that you can elegantly place together is most fully realized with pure functions. Async enables more of this style of modularity in Rust (but not without some cost which is why it's debatable whether or not it's better than go style concurrency).
Do you mean on a hardware level? Because otherwise, hell no.
If I’m writing a binary file parser that buffers some data and processes it, I sure as hell want to wait for IO, there is no other meaningful way forward in most cases. It is also not a small isolated part, but a tight loop bouncing between CPU and IO.
Async is a function of the caller.
I mean in the context of async await. The smallest primitive that is properly async is an IO function.
There are functions, you can call them sync or async if they handle IO or UI or they will get a necessary data later.
I still don't understand why I can't fetch a URL from top level javascript. Also I don't understand why zig async and await passes the control flow that seemingly total arbitrary way. The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way?
Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript? Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines? I don't know. Anyway async as is used with colors is totally against the picture that the words "async" and "await" suggest.
> Maybe javascript async is build upon regular promises and regular objects, instead as a proper language element with proper support in the javascript engines?
Yes, they are promises.
> The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
> The naive approach (put async calls in a queue, and periodically check if they are completed or can be executed) seems fast, deterministic, and good enough in every way? Okay, maybe zig needs the speed, and can't just stop the execution of the sync code time to time to do something else, but why javascript?
You're describing preemptive multitasking. You can create separate execution threads pretty easily in most languages (even super old C code), but that's not why Javascript and Zig have the 'async' keyword. The difficult part isn't the asynchronous execution; the hard part is handling the result from asynchronous code (i.e. the 'await' part). Promises are a simple mental model for organizing how pieces of code depend on the results of other pieces of code. The C# documentation does a good job explaining this:
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous...
Note: From Zig's documentation it sounds like 'async' is cooperative multitasking. This is single-threaded execution. It is "concurrent" code, but it isn't executing in "parallel".
Concurrency is difficult because it is explicitly resource management. You don't need concurrency for calculating the correct answer, you only need it for managing time.
Sending a letter and getting a reply is an inherently async action; you can stare at the mailbox all day but you probably don't want to. Waiting in line at the bank is inherently sync; if you try to do something else and then come back they'll make you start over again.
Yet, it's up to me to decide, not to the mailbox.
Caller can use this information to invoke 10000 IO bound tasks in parallel, whereas cpu bound tasks are limited to number of cores and may want to be throttled. In your analogy, I can eat a donut while simultaneously listening to music, but I can not eat a donut while simultaneously eating fish soup.
Now where it get complicated is when a function does both io wait and heavy cpu processing. Now neither color of the function match.
Forcing this upon the caller to invoke such functions in special ways is the strange part. Taken to the absurd you ought to have special syntax for every resource constraint in the computer, memory bound, disk bound, wifi bound, and so on. It would be better if the runtime could figure this out by itself and parallelize accordingly.
The single threaded aspect of async functions does have certain other benefits, like not having to care as much about muxes when synchronizing state.
Without it when you want to do asynchronous stuff you either block on it (not good since JS is single-threaded), eg "let toto = asynchronousFunction(); doSomething(toto)" and nothing can be done during the time asynchronousFunction is waiting on IO or something (in a browser environment that means part of your website stops responding to user input); or pass as an argument to the asynchronous function a function that will be executed by the asynchronous function once it "wakes up" (usually called a "callback"), and while it's waiting, the JS runtime can execute other stuff, eg "asynchronousFunction((toto => doSomething(toto)))".
Callbacks were I think from the start in JS. After a while promises were introduced, which compared to callbacks avoids nesting, and probably have some other advantages that I don't know about. Still, they are method chaining and not "regular code". For example regular try/catch won't work as usual, you have to use .catch(). Even later async/await got introduced, which allow you to write asynchronous code as if it was regular code, with a few exceptions (for example you can only use await in an async function, top-level await took a while to land on Node, stuff like that).
I don't think using real world human actions helps with understanding any of that. Async functions make sense in JS because again, JS is single threaded and if you do a blocking call (like asking the kernel to read a file, or making an HTTP request and waiting for the response), the event loop is blocked. From Node.js documentation's on the synchronous function in the fs module:
> The synchronous APIs perform all operations synchronously, blocking the event loop until the operation completes or fails.
I feel like I need to point out that Donald “Structured programming with GO TO statements” Knuth included an example of coroutines in the first volume of The art of computer programming, the first edition, dated 1968. In assembly language for an accumulator machine. With a box of scraps!^W^W^W^W^WThat is to say, that C and most other languages have made coroutines awkward and thus virtually unused in the past three decades or so does not mean they are particularly novel or high-level.
Granted, Knuth used coroutines in a simulation and not for I/O, so he did not need that much of a scheduler, but still.
It's just an abstraction, it's not zero-(runtime)-cost. It might be the "lowest possible cost", still nonzero.
I continue to find the "function coloring" argument misses the point unless you're arguing from a developer experience perspective. Why should two fundamentally different things look and function the same? Want this the ultimate pitfall of early RPC implementations where everything looks synchronous?
In Rust, a lot of the friction is due to how async functions/futures fundamentally differ in model of execution and how that interplays with the rest of the language. Other languages get to hand-wave a lot of these problems away with a GC. (It certainly could be less of a pain nonetheless.)
- Futures don't execute until polled, can partially execute if something isn't ready (and would block), can partially execute and be arbitrary cancelled and dropped. There is no equivalent in sync functions unless you make them co-routines.
- Since you can "pause" futures if something would block, you need a place to store the function's state for when it's resumed. Now you must be consider if the async function's state is `Send` if the future wants to move to a different thread to continue executing -- which is why you see `pin!` used. Sync functions don't care about this since you always run to completion on the same thread, and the stack is ephemeral.
- Likewise, the `Future` returned by the async function is going to need to encapsulate it's state that it saves. In the general case, this is a compiler generated anonymous struct that changes if any state saved across `.await` changes, hence the opaque `impl Future`. This is why you see `BoxedFuture` a lot to abstract this away at the expense of an allocation. Ideally, the new associated types with lifetimes can avoid this with traits.
So if all functions were co-routines (i.e. functions that can be resumed and re-entered) they would all have the same "color". But all you really did was "lift" all sync functions to be "async" functions with no internal await points.
(IMHO, if the C# team back in the day decided to implement full blown co-routines into the language instead of just `async/await` as a compiler trick, I think many other projects would have followed suit with a more general co-routine solution instead of treating `async/await` as this special thing which is just a specific compiler generated implementation of co-routines.)
async in Rust is a popularity issue even more than a technical issue.
A whopping amount of people who are coming to Rust are doing so because they want a good ecosystem for implementing network service servers. Rust/cargo/crates hits the sweet spot for them.
I am with you that I loathe async/await in Rust. However, I also have to acknowledge that without async/await, Rust is a vastly less popular language.
All the other uses of systems programming are simply dwarfed by number of people building network services. That's just the sad reality.
Part of the reason was Rust's desire to show itself as a direct competitor w.r.t performance, I think.
Say you use a vector in C++. You push one element and pop two. In Rust that's a None. In C++ the answer is UB (in my case 43).
That prioritisation applies to the whole ISO language, not only to the standard library.
https://godbolt.org/z/vYcMhE9h7
What I'm talking about is that Rust's Option is very cheap in all the cases where it can be very cheap, which makes this whole design feature more affordable. C++ eventually grew std::optional which is not powerful enough for this work and yet is also bigger and slower. They could have done better, but safety wasn't a priority.
Unlike C, which the only way to be safe is not to touch it at all.
As for performance, ISO doesn't implement compilers, there are many ways to improve performance while keeping the semantics in line with the standard.
If the compiler vendors decide to focus elsewhere is another matter, a bit like there are languages as complex as Rust and compile faster, because that has been a concern, whereas rustc developers have their concerns elsewhere.
Even your dynamic example won't tell me calling pop/push x/y times respectively will fail if x > y.
I have to trigger it somehow.
I haven't written the Language of Kings in a while: does GCC -Wall truly enable all warnings yet?
clang has a -Weverything and it isn't supported.
The argument is that Rust forces you and in C++ you can forget/the compiler can do what it wants
It's like Yaml parsers, they had "load_yaml" which is unsafe and "safe_load_yaml" which is the secure option. Imagine no surprise when Rubyists went for shorter safe looking method and got their servers pwned.
Depends what you mean by explicit.
Look at it like this:
In this case you see the value is missing. will panic with "I want a value" because value is empty. And most rigorous way to deal with it is: will either print value or EMPTY depending on what the vector contains.Types like Result will issue warning that you didn't handle the cases.
I looked at your example and played a bit with it and yes I agree with you - compiler does help in this case.
My old text:
So instead of checking if vector is not empty you check that the return result is not empty. I do not see much difference. If Rust compiler would choke when "else" clause in your example is not present I would understand your point about compiler preventing improper access.
> If Rust compiler would choke when "else" clause in your example is not present
The compiler won't choke, but it will stop you from accessing the value.
It doesn't matter if you omit the `else` clause or not, the type system ensures that you can't access invalid values.
Here's a bit of an example based off of @Ygg2's code: https://play.rust-lang.org/?version=stable&mode=debug&editio...
Hacker News will try and discourage really quick back and forth comments. To do so, the reply button is hidden. However, if you click on the timestamp, to go to the comment's own page, you can reply there.
Rust does that with the combination of "sum types" (aka "enums with data") and pattern matching. An `Option<T>` is either `Some(T)` or `None`. Matching on an option with the pattern `Some(value)` creates a new syntactic scope where the value is accessible. This scope is only entered if the `Option` is actually non-empty.
All ways of getting the value from the option are ultimately shorthand for a match in that way. For example, option.unwrap() will either get the value if it there, or panic. Option.unwrap_or(x) will either get the value of it there, or use x instead.
In practice this is x100 less error prone than C++. Source: was burned by vector.front() on empty vectors more than once. In C++, UB. In rust, usually the "empty case" is considered when first writing the code, or at least caught during review (unwraps tend to be very visible in reviews)
I'm not a C++ expert but here is my understanding. Vector is essentially a tuple of (dynamic_array_address: ptr, size: size_t, capacity: size_t). To pop a value from vector you just decrements size. So what happens when you have empty vec? Your size is 0, and you're substracing 1, which causes undefined behavior.
Correct way is to check BEFORE you pop_back().
In Rust, any number of invocation of pop() will not result in undefined behavior. You can ignore the value, or you can check it, but it doesn't expose you to UB just for slightly misusing a vector.
Absolutely and this is exactly what I do. I always check the containers before removing elements.
>"In Rust, any number of invocation of pop() will not result in undefined behavior. You can ignore the value, or you can check it, but it doesn't expose you to UB just for slightly misusing a vector."
I do not understand "slightly misusing" part. I assume in Rust it would bomb if pop() returns "none" and you try for example add said "none" to some number.
That's the thing. It wouldn't. Worse you can do is make whole program panic, with well defined stack trace and message.
It will never return non-sense and pretend all is ok.
https://godbolt.org/z/vYcMhE9h7
> Error: attempt to access an element in an empty container.
It is also what attracted me into C++ coming from Turbo Pascal and Turbo Basic, back in the early 1990's.
Although C++ culture could be much better towards safety, it is definitely better than whatever WG14 is doing, or C has brought into the picture for the last 50 years.
Also anyone that just copy pastes C like code into C++, is the kind of developer that will be using unsafe{} all over the place, on the languages that have them.
C developers like telling themselves that only people with bounded rationality make security critical mistakes. All the skilled C developers have ascended beyond the mortal realm and would never let themselves be chained up with crutches for the weak like affine types or overflow/bounds checking.
Yes you could. But I do not think " cultural" is the right term for not putting all your code in an `unsafe` block
It's not. It's a safety phenomenon. See Java, C# etc.
Java doesn't have huge focus on it, but managed to get it right.
As for your dynamic arguments request, have fun,
https://godbolt.org/z/YYKrnh4Y9
In a runtime example I can run it with tests and it would behave fine if both values are same or first arg is bigger, in Rust's case it would behave valid for ANY combination of arguments.
Coming from Java the C++ stuff is jarringly unsafe.
Still, if you want to contribute to OpenJDK internals, they will only be taking pull requests in C++, not Rust.
So better learn how to build those seatbelts and airbags, for all the projects we rely on, including Rust's own reference implementation, that aren't going to rewrite their code from C++ into something else.
> It's easier to work with than C++, but that's fairly faint praise.
So, while Rust positions itself as a C++ alternative with all the complexities that C++ developers love, it may become less attractive for other use cases. For instance, I find it highly questionable to develop a web app in Rust...
Rust as a C++ replacement serves a real unmet need in the marketplace.
Knowing what we know now, we should’ve gone with TypeScript and just written our own Odata filter on top of it, but live and learn.
If I was in a position where I could pick and chose languages, and not worry about how not having TypeScript in most things will mean our best front-end developer can never go on vacation because he’s sort of our only front-end developer, I wouldn’t mind using Rust for web-backends.
Rust GUI is obviously not a great experience. At least not yet. But it’s not that bad either. I think it’s mostly the case of how JavaScript is just so good at it and seeing such a fast pace of improvements because the entire world uses it for most GUIs these days, that it’s just hard for anything else to compete. I mean, look at stuff like Flutter or Blazor, they are backed by Microsoft and Google and they’re vastly inferior choices for most use cases compared to simply building things in React, ReactNative or even electron, and that’s not because I have some wild love for JavaScript, it’s because it’s seeing rapid improvements they dwarf it’s competition simply by being used by a lot of people.
I wish Rust would have someone like Facebook pick it up and build a frontend framework for it, but I think that is just too unlikely for you to bet on, and you certainly wouldn’t want to do it yourself, even as open source because then that would probably be your entire job.
On the flip-side, the packages that handle basic back-end web stuff for Enterprise use are rock solid in Rust. Which is impressive, at least to me, considering it’s young age. I have no idea why, but maybe some serious players are contributing to it because they use it themselves. There isn’t a “Django” or Ruby+Rails for Rust, but if what you’re building is a lot of smaller APIs with various transport methods and data access in an federated authentication scenario then Rust is surprisingly mature for the web. It’s primary disadvantage being that your TypeScript and Rust developers won’t be able to cover for each other (which is why we didn’t poc with Java).
We have yew, and my personal favourite: leptos (https://leptos.dev/)
I mean who'd want to and why bother trying to support that use case? Basically none of Rust's value proposition exists for a web app while nearly every single one of the downsides do.
The single biggest problem I've run into is that there's no real good story for a web app framework (especially since everyone's gone crazy over async). Rocket is perhaps a bit too magical for rust folks and it's been abandoned, but I rather liked it. I've warmed up to axum, but all this async stuff still rubs me the wrong way.
And, yes, compile times still suck.
The number one syntactical beef I've is operators. e.g. == vs ===, ""+number.
Mono is mostly only relevant if you need platform specific capabilities that wont be covered in .NET.
I'm talking about things like exhaustive "switches", built-in, ENFORCED handling of "missing" values (null, undefined), and finally useful error handling.
I actually don't need any of the baremetal features of Rust. It's just that most of it's "zero-cost" abstractions are still far superior to those of other languages.
(Some dislike it, but Java’s checked exceptions are exact homologous of result types)
Outside of that, yeah they just differ in very low-level aspects that most developers don't even know about.
Do crates have namespaces that ownership is verified for?
I am constantly baffled that NPM, PyPi, crates.io etc. didn't copy this idea for those last two reasons. In my mind, it's not quite best in class without it.
[0]: https://central.sonatype.org/publish/requirements/coordinate...
[1]: https://central.sonatype.org/faq/how-to-set-txt-record/
[2]: https://central.sonatype.org/publish/requirements/coordinate...
[3]: https://internals.rust-lang.org/t/pre-rfc-formal-squatting-p...
[4]: https://blog.sonatype.com/this-week-in-malware-may-13th-edit...
cough C#. You also get LINQ. And a fairly heavy amount of web frameworks in ASP.NET / Razor.
The pieces and features i wanted exist but tying them together was not always straight forward.
From the top of my head I used:
- actix-web
- tracing (logging)
- sea-orm
- config from yaml files and a custom implementation to allow profiles.
- utoipa (for openapi and swagger ui)
- thiserror
In a professional context I would not have used Rust for this project, but it was quite fun to explore it's possibilities and rich crate ecosystem.
I write my web apps in C++. They tend to be little bit more than just query database / update database. They're exposed as JSON based RPC and can be accessed by JS front end from browser or third party systems we interact with. The performance is stellar and the code size is not much different comparatively to using PHP / Ruby / Python / your_pet_goes_here.
But we already have plenty of options for this use case (.NET, JVM, Go, Python, Ruby, node.js, PHP, Erlang, etc. etc.). Very mature ecosystems that solve a lot of different edge cases.
There are very few C++ alternatives worth mentioning, none as mature as Rust in terms of adoption/tooling.
If you need to write a webapp that would require C/C++ kind of memory handling/performance then Rust would be the ideal candidate I think.
I don't know what kind of tradeoff matrix makes you use the Rust graydon describes over the existing options.
For example my main issue with D (but TBH that's something I've given up on actively tracking 10 years ago probably) is that by including GC in the runtime/stdlib - it basically painted itself as a poorly supported competitor to C# rather than a C++ replacement.
Indeed in software engineering there is no silver bullet nor a perfect language for everybody :-)
https://www.cs.bu.edu/~hwxi/atslangweb/
> (Swift at least traps in release by default -- I wish Rust had chosen to).
I enable it in release on serious projects:
It's very annoying , but I use it in code where this is critical.
incorrect crypto code. If overflow is intentional, it should be annotated as such in operations, will generate similar assembly and won't panic. If it isn't intentional, then the code was bad to begin with.
Rescript lets you skip most of those, plus the (overblown but also real) weird syntax. I like ocaml and use it for projects still but if you just want to play around with what's unique and strong about it, rescript is what I would recommend right now.
I’ve always seen safety and lifetimes and borrowing as the main value prop, so I was surprised to see he was sort of aiming at an ML without GC, rather than a C++ that doesn’t blow up.
https://www1.icsi.berkeley.edu/~sather/
I recently revisited it for Advent of Code (where part of the challenge was getting the compiler itself to even build with even semi-modern tooling), and a lot of the above features still have value.
What it BDFL?
"Benevolent dictator for life (BDFL) is a title given to a small number of open-source software development leaders, typically project founders who retain the final say in disputes or arguments within the community. The phrase originated in 1995 with reference to Guido van Rossum, creator of the Python programming language."
What alternatives are there?
What is the rust experience wrt to inlining? Can every expression be inlined or only selected ones? How can you know what got inlined in some expression? Do you have to manually annotate every single function call you have to inline or is there a more general command?
Stdlib containers are all in the generics category.
Is this a problem is rust? Is too much/too little marked inline? What if you really need to inline some function from some library that was not marked inline by the author?
If you implement these things in the compiler (open code means emit the implementation inline as you go, can also emit calls to the compiler runtime which is roughly similar to library code that the compiler ships and knows lots about) then users need to hack the compiler to change them.
However, if the structures are in the compiler, and you've done things like encode them directly in the AST, the compiler has a better chance of emitting useful diagnostics for them and of optimising them at the semantic level of the container.
C++ goes with library code supported by compiler intrinsics, and a common developer experience is compilation errors referring to iterators some distance into the library code. It also can't sanely do things like call reserve on a vector outside of a loop, because by the time it's ready to optimise things it's holding raw pointers with mangled names, not a hashmap instance.
Conventional wisdom is to put containers in the stdlib. D has some support in the compiler. I'm starting to think this is one where conventional wisdom has got it wrong.
Anyone writing these things today in C or C++ already understands object lifetimes and Rust just adds a static checker for them.
In such projects churning out lines of code is not the bottleneck, ease of development should not be prioritized over long term maintainability.
Why on earth would you try to rewrite python CRUD apps in Rust?
As someone who only used Rust casually - understanding object lifetimes and knowing how to encode this in Rust type system is not the same thing. Not to mention that Rust can't statically prove some things that are valid (eg. cyclic references).
I've certainly seen seasoned C++ programmers saying this. Of course, a few of them of say they understand object lifetimes so well, they they don't need the static checker!
> Why on earth would you try to rewrite python CRUD apps in Rust?
This is one of the great mysteries of our times. I do think the enthusiasm for using Rust for web apps and such (a) is misplaced and (b) has been a drag on Rust developing into a better C++ replacement.
There is a difference between understanding lifetimes and being able to keep track of them. I have a lot of objects in my more than 10 million lines of code. Most of them have simple lifetimes that are easy to track, but a few for reasons (which may or may not be valid - often the reasons are it was built in C++98 and updating to modern lifetimes is hard when it is used all over) have complex lifetimes that are tedious to track. It isn't that I can't, it is that I get bored/make mistakes and the static analyzer wouldn't (Or course C++ can't be statically analyzed, but if it could the static analyzer wouldn't fail for the same reasons I fail)
That's why we're stuck with lifetimes as function contracts.
Because Rust has a lot of incredibly helpful features that make bigger systems far less of a pain to maintain. I work in Java/Kotlin, and my entirely gut-based estimate is that 66% of problems wouldn't happen in Rust.
My big favorites are: - Sane, well-defined, enforced, opt-out error handling - Sane, well-defined, enforced, opt-out handling of "missing" values - Exhaustive switching - WITH usable ADTs (enums) for encoding valid state
None of the web languages I know have all of that.
But even Java is pretty much there: checked exceptions are available (and imo superior), but Optional is also an.. option. Kotlin/scala can handle nulls, but java can also statically analyse every usage with annotations.
switch expressions are exhaustive in java over enums and sealed ADTs. (Rust has a misnomer, their enums are ADTs, java has both real enums and ADTs now).
I also found this bit interesting: "It's easier to work with than C++, but that's fairly faint praise".
I see this kind of thing in my own personal projects all the time. I'm thinking "oh it would be really cool if I built X" when in reality most of the time users just want really simple stuff.
Being easier to work in than C++ might be faint praise, but it's probably the biggest draw of Rust for me. I don't want to touch C++ with a 10ft pole, but I love using Rust.
Overall, a really interesting article. Though I like today’s Rust, I do think I would prefer the trade-offs made by the alt-Rust outlined here.
Perhaps it’s just my own personal preference, but I think there is a strong bias in users towards what they are already familiar with, and it’s hard to break away from those without a BDFl or similar position of authority who can impose their vision.
I think whether you view that as benevolent or not (I would, because I agree with lots of de facto development standards being detrimental to language quality, but I understand that that is my opinion and not hard fact) is in the eye of the beholder.
For example rust in it's current design relies quite a bit on the combo of: generics monomorphisation + inlining + dead code elimination. But this reliance doesn't play well with an stable ABI. Similar features like generic associated types and similar do not make the story easier. Like an rust stable ABI likely would only support `dyn Trait` and no generics or "on the fly" provide a `dyn` variant for generic method where possible. But even that isn't really good enough for a lot of use cases in a lot of different ways. Additionally a ton of important rust things are not `dyn` compatible at all.
Even some of the "easy" to solve things aren't that easy for non-technical reasons. E.g. a lot (but not all) of `impl Into<T>`, `impl AsRef<T>`, `impl Borrow<T>` etc. cases are best handled for a stable ABI context by aplying the single function of the trait (`.into()`,`.as_ref()`, etc.) _before_ calling the function and only having an ABI stable version for that. The side of which traits qualify for this is easy (you annotated the traits) the "when not apply it even if it seems valid" part isn't that easy not for technical reasons but for communication/documentation/avoiding unexpected outcomes reasons.
I don't understand the subtleties here: Is tail calls an optimization the compiler can do irrespective of the language? Or is there something that prevents this and requires the compiler to use stack here? Is there any visible effect to the programmer from supporting tail call or not, other than performance and stack depth?
How does not supporting them compete with C++ performance?
If rust says they don't support it, does it mean they don't even allow the compiler to do it?
Obviously the syntax of the language itself already supports calling the function itself, and whether tail call optimization is supported or not doesn't affect the visible result afaik (except in case of stack overflows / performance)
Or like xmtp or mail or whatever, you generally write your client assuming you are going to talk to any spec compliment server, which can stop you from using some extensions if you aren't sure they will be supported.
Possible is not the same as guaranteed.
Some constructs cannot be optimized for tail recursion. Languages with tail calls have guidelines of how to must write your code to ensure the tail call happens - often a seemingly small code change is the difference between tail call optimization happening or blowing up the stack.
Second, in at least some cases where the optimizer can apply tail calls you need to be guaranteed it will happen. Nothing stops a C++ optimizer from applying tail call optimization (I don't know if any do, but it is allowed in some cases), but the language doesn't require it, so even if your optimizer supports it you can never know that it happens - and more importantly you cannot be sure that after changing the code or upgrading your compiler you will still get it. Thus even if your optimizer supports tail code optimization you dare not do deep recursion.
If your language doesn't have support for tail calls you cannot do deep recursion with confidence. If your language does, then you can do deep recursion so long as you follow the rules of the language. (whatever those are - I'm not up on the latest research here, so I don't know the state of modern tail recursive languages are.)
Now, 256MB is pretty huge, but we probably want way more memory kept for heap storage and similar things (because managing lifetimes purely on the stack will be hard and might require a lot of copying of data), so it will be less than that. Now, you may ask, "Why not start with small stacks and make them bigger as needed?" It's a good question, but since our stacks are contiguous areas of memory and we don't know what's in them, we still have to space them out in our memory map allowing for as much growth as is needed.
We might solve some of these problems by introducing segmented stacks, but this is one of those problems that crosses so many language, runtime, and OS boundaries that it's been hard to do in the general case, and it feels like gently nudging people towards writing code that can be tail-optimised is easier, just as it's easier to push people to use async than it is to provide systems and APIs that would allow for blocking code and a huge number of threads.
The problem is adding TCO to a language that was born without it. It won't break existing programs but you'll be able to run a program using TCO only with a specific compiler or interpreter from a given version. It could catch up if the main implementation of the language does it, but often we care about compatibility with the alternative implementations. Think about Ruby and Python, MRI and CPython and all the other implementations of the language. If I'm not wrong, MRI added TCO under a runtime configuration variable but JRuby doesn't support it.
Therefore _yes_ the compiler can always do it, but it may involve patching the called function to do some work. Splicing code into it and/or changing the calling convention. That's difficult to do for unknown caller/caller pairs, e.g. function pointers or separate compilation to machine code without enough metadata to patch it later. It runs a bit close to "sufficiently smart" compiler which usually means doable in theory but unlikely in practice.
If you pick global designs to make them easier - probably most notably having the callee clean up the stack frame from the caller - and that makes other things slower, then you've given the competition an edge.
Clang now has tail call annotations for C++, which works mostly because the compiler can reject them in the cases where it hasn't implemented the lowering. Rust can probably have it in the same approximate circumstances as C++.
[1] https://github.com/rust-lang/rfcs/pull/3407
Isn't that a matter of self selection? A language which developed around those priorities would have a community sharing those priorities now.
The point is if that community would be as large as the current one, larger, smaller.
Interestingly like Graydon suggests this and 'tis something D has, you can only have `ref` for function parameters. This is something the users sometimes complain about, but I guess it has positives.
I find Rust basically unusable -- at the level of abstraction I want to write code, basic definitions break line limits.
Rust seems to be a repetition of C++'s mistake: a language which conspires you to pretend it's another. There are now nearly as many Rusts as C++s.
If I return to any domains where Rust would be relevant, I'd probably now opt for Zig or equivalent.
via https://github.com/RustPython/RustPython/blob/main/jit/src/l...
Have a look at real-world rust repos that are more than simple application code.
Personally, I feel like I'm being visually assaulted.
Some of your potential syntax changes seem less readable but they might look nicer to you such as changing `attrs.clone().into_object()` into `clone attrs as object`. You probably come from a python background I would assume but the rust version is easier to parse and understand but maybe to you not visually appealing.
Having an explicit return argument I could understand. I found this weird in the beginning but this is something I got accustomed.
Not sure why square brackets should be favored instead of angle brackets. The imports in the Rust version are also very explicit and fine I think.
Replacing `()` with `None` seems silly, same with replacing `Ok` with `ok`.
Maybe the `py_to_serde` arguments of the function definition spanning 4 lines could be considered unastethically pleasing. This is something I noticed quite a lot with Rust code. Fmt also seems to break to multiple lines quite fastly when writing function or method definitions.
I replaced () with `None` (& with ref, ! with run, etc.) because I want code to read like english (ie., literate) all other things being equal. I dont find `()` "pays the cost" of illegibility by syntactical convenience.
Changing "operator-like methods" to operator words would have a radical impact on the parsing precedence and "overall look" of the lang. ... so `clone ...` would be more readable if those changes were made. The advantage of many keyword primitives is that you dont keep reusing the same syntax for everything... syntax is there to support expression. I think more is better.
`Ok()` to `ok()` was more a broad notational philosophy of basic type constructors being unobtrusive.
Scala has gone from C-like (v2) to python-like (v3) syntaxes --- and I think the ML-ish whitespace, python-ish "beat poetry" approach is mostly just better.
I buy some arguments that symbolic redundancy can help (ie., both indent and use symbols)... but Rust's syntax philosophy is clearly to "keep adding symbols", and I dislike it.
C uses symbols, but I do think C is quite beautiful mostly -- because it's so simple. Rust is creaking under the weight of its size and symbolic choices
When writing code you are pattern matching and certain special symbols such as `()` over `None` and `attrs.clone().into_object()` over `clone attrs as object` are just faster to detect at an instance.
Using more english prose becomes a word salad that is harder to parse than using special symbols which convey meaning. Certainly more familiarity with a programming language and it's syntax elements will help you your pattern matching mechanism and allows you to understand code faster. In my opinion Smalltalk gets it quite well in this regard.
I also would rather prefer lisp language syntax, or apl.
You seem to want contradictory things — if you don’t need that level of control just use any of the litany of high level languages with nice syntaxes. I don’t think we can eat this cake anytime.
This makes brining in the "full power of dynamic languages" almost trivial and extremely performant.
Delete half of rust's symbols, get rid of its macro system, rewrite it until lifetimes are inferred /or/ allocators are explicitly chosen, etc. etc.
I don't want to feel visually assaulted when writing the type signature of the sort of function common in a dynamic language. Here `mypy` is also treasonously guilty.
Consider, eg., zigs "function which returns a type at compile-time" is an example of where blindingly-obvious syntax retains its blindingly-obviousness because of compile-time eval... ie., we dont need a "second syntax" to program the compiler.
This "two syntaxes, one for the runtime and one for the compiler" approach -- has swamped Rust as it aims for greater expressiveness. Not least, because it has a third syntax: one for unsafe.
Safe, Sound, Complete 99.999% of the time; a joy to use 100% of the time
What are you referencing, concretely? For example, "unsafe" doesn't add or remove syntax.
And you'll find about "4 languages" all mixed together.
Most "syntax" in AOT, statically typed languages does not directly generate machine code, but it does directly impact what machine code is generated. So there's not a clear distinction in practice.
For example, a lot of the syntax is used to control method selection and verification - that's not a "different" syntax or language by most folks' definitions.
Or below, let's invent a language where there's (in my sense) "one syntax for everything",
Eg., consider something like,
Here you can see that code with ordinary run-time semantics is used for the compile-time operations of generating a trait and specifying that a class implements that trait (ie., we call the trait-defining function on the class).Whereas in Rust, the syntax for returning values and "quantifying over types" is radically different.
This is the historical approach (C++, C# .. almost all langs) -- but not one modern innovative langs follow.
I think with as much desire for novelty rust has, cramming it all into its "compile-time syntax" has hobbled it.
A simple uniform language for both c-time and r-time could be used
One of the reasons that something like unified syntax is not preferred is because there are some important distinctions between runtime and comptime, and a lisp-like syntax doesn't translate well because lisp doesn't have a meaningful distinction between the two.
For example, it's desirable for some folks to have the "comptime" work (declaring types, imports, function signatures, etc) to have a purely declarative syntax. And languages like Rust use declarative syntax to express things about types, interfaces, and their constraints. Contrast that with the logic of the program which is more convenient for a programmer to express in the imperative style.
Feel free to do this. I predict you will end up with a language that appeals to nobody but yourself
I imagine it has more users than just me.
My version would, of course, be Nim++
Show us an extant language that does this, then.
Partial Evaluation (automatic) was a very unsolved problem, last I looked.
The fact that it is a law-level language best fit for system programing came much later. Late enough that there are people here and there still trying to use it for the original use-cases.