And indeed, I think we're the only agentic coding environment with jj support.
The most difficult code in the 1.0 release is some gymnastics to avoid the appearance of a concurrency conflict with a user running their own jj commands, made at the request of the person who introduced me to jj.
Ooh. The answer is probably more interesting and philosophical than you expected
I can tell you that we do extensive testing, we figured out how to objectively measure the code quality on certain benchmark problems, empirically it's extremely helpful nearly all the time.
But in the general case: it is not actually possible to guarantee this.
That's because whether a change improves the code often depends on information which is literally not present in the codebase.
Some of these are more trite. E.g.: whether a comment is helpful or redundant slop depends on the audience.
A simpler example: There's a function that's never called. Should it be deleted?
There's a number of factors outside the codebase that determine the answer. Including the obvious one "Not if your next prompt is going to start using it."
You found a way to objectively measure code quality?? Sell that! Why even sell this course when you have the ability to literally beat every software company?
In honesty, that's not a bad idea, and we hadn't thought of that.
It's pretty expensive to measure even for small programs. It's also more of a relative than an absolute measure, i.e.: it scores two variants of the same codebase, but the raw scores aren't very meaningful on their own. So our goal had been to use this in the benchmark set we're working on when we release a standalone refactoring product.
But the more I think about this suggestion, the more I think: "Hmmm, why not?"
I started playing didgeridoo 10 years ago for precisely this reason. Sleep apnea already cured by weight loss, but I knew by air pathways were prone to it, and I never wanted it to come back.
It worked
It took me 1-2 years to learn circular breathing, but even just learning to play for 15 seconds on one breath can give the "oxygen high" from breathing so much.
You can't get "oxygen high" from breathing normal air. The O2 levels will always stay the same unless you stop breathing for a while. What will make you feel weird in the head when breathing too fast is the reduction of CO2 in your blood.
One a friend and I hooked ourselves up to continuous pulse oximetry and had a contest to get the lowest recorded oxygen level. We tried everything we could think of, from just holding our breath to end-expiratory breath holding to hyperventilating to clear O2 (I used to do some recreational free-diving) beforehand to exercising (jumping jacks)...
Neither of us could get it below 98%, and this was at a mile of elevation (UNMH in Albuquerque).
It is pretty easy if you use the Wim Hof method. Breathe deep and fast for a few minutes, to the point where you get dizzy or feel weird sensations. Then exhale and stop breathing while being fully relaxed. I've done this while hooked to a pulse oximeter and it takes quite a while before O2 actually starts dropping (especially because the effect can be delayed in your limbs), but once it starts you'll pretty quickly run into the regime where a normal oxi will start an alarm because O2 is too low. You can even go below 85% without losing consciousness, because your limbs will desaturate faster than your brain. It's also not uncomfortable, because rising CO2 is what causes breathing reflex, but you dropped its levels far below the threshold by hyperventilating first.
Interesting. As I noted above (though typo O2 -> CO2), I used the same technique you describe, which I learned in free diving, and was not able to get below 98%, at altitude.
I'm quite sure no freediving instructor would ever teach you this particular method, because it is a surefire way to die underwater on your first attempt. Free diving breathing techniques usually revolve around lowering your heart rate, not lowering CO2. Wim Hof trainers will also tell you to never to use this method when near water.
As for the specifics that may have prevented you from doing what you wanted: If you breathe too shallow or too slow, you won't clear enough CO2. In freediving this is normal (even wanted), but for Wim Hof practice it means you didn't do it right. You really have to breathe so deep and fast that you enter an uncomfortable zone. It's not unlike physical exercise, except it's mostly mental.
> I'm quite sure no freediving instructor would ever teach you this particular method, because it is a surefire way to die underwater on your first attempt
Definitely no instructor involved, just a dumb 20 year-old living in Puerto Rico. It admittedly was dangerous, but I am living evidence that it was far from a "surefire" way to die. It was one of a hundred ways in which I put my life at risk during my 20s. ¯\_(ツ)_/¯
> As for the specifics that may have prevented you from doing what you wanted: If you breathe too shallow or too slow, you won't clear enough CO2.
I'm confident I was doing it sufficiently well to accomplish a longer period of breath holding than I otherwise would have been able to sustain, as evidenced by having done so (in addition to the usual symptoms of lightheadedness, confusion, loss of vision, near-syncope -- yes I agree quite uncomfortable). I know people on HN love to idolize Wim Hof, but in this context minute ventilation is not that difficult of a concept; I'm usually able to estimate the response in a paralyzed patient's PCO2 fairly well when making changes to their tidal volume and rate.
I didn't search for too long, but here's at least one relevant document, in which otherwise untrained subjects were able to achieve a substantial reduction in CO2 (17.4 vs 29.0) with a mere 15 seconds of hyperventilation, leading to an extra 23 seconds of breath holding prior to involuntary breathing moments. The peripheral O2 sat nadir in the hyperventilation group appears to have been identical to the non-hyperventilation group after the first trial (Fig 8b, looks like ~94%) and was only statistically significantly lower on trials 2-5: <https://pmc.ncbi.nlm.nih.gov/articles/PMC10363065/>
90 seconds of breath hold is still way too little to see oxygen drops in a finger pulse oxi. Also explains why you didn't die free-diving. Even with no training you can go much longer than that before risking a blackout (at least under normal circumstances). In fact training free-diving is all about CO2 tolerance and relaxation, so you won't even be able to store more O2. When I was reaching the 80s, I was doing 3+ minute breath holds. The first ~2 minutes my oxi stayed at full O2 sat with basically no change. In principle you should be able to induce a blackout yourself using this method without feeling an urgent need to breathe - if you do the prep stage hard enough. Just make sure you only do this when lying down in a safe position.
Can’t claim to know for sure, but I’d assume some kind of measurement limitations: the resolution and upper/lower limits of pulse ox are probably calibrated to some medical need, not to detect changes beyond what’s medically necessary
This was using equipment in the emergency department of our state's only L1 trauma center and comprehensive stroke center; I presume it was decent as far as medical monitoring equipment goes.
If you've ever done the wim hof breathing method, it is a very intense experience.
Basically hyperventilation + long breath holds. Probably similar to what free divers do without the mammalian dive reflex due to the cold water. Or like a dangerous game kids used to do when I was in school where you hyperventilate and then have someone press on your chest until you pass out.
But anyway, I'm not sure if the science would back it up, but Wim Hof describes it as over oxygenating the blood and then stopping and letting CO2 ramp up or something. Whether it is significantly dropping the CO2 or increasing oxygen during the hyperventilation phase, isn't it kind of the same thing? Adjusting the ratio.
Anecdotally, when I was doing it regularly I seemed to not get sick at all.
Blood oxygen saturation is always near 100% in a healthy person. 95% is the low end of normal. Dropping to 90% is considered hypoxemia, and 80% is a medical emergency. So there really shouldn’t be any room to increase it significantly.
In many people a momentary drop to the 80s or even below is not an emergency or anything close to it. Not saying that it is good.
Someone that is awake, sitting up, and struggling to breathe should be considered an emergency regardless of oxygen levels (and in this situation 80% would be very concerning).
EDIT: your comment is otherwise entirely correct, particularly at sea level.
No it would stay at pretty much 100% (as is normal). But co2 goes down, which lessens the ability of oxygen to come out of the blood. That's why you get dizzy when hyperventilating
Nope. The blood is already fully saturated with oxygen (in a healthy-ish person) at rest. Even intensely breathing pure o2 can't give you a saturation higher than 100%.
I don't see how. Cases are decided by facts and law, not feelings -- except to the extent those feelings are probative of state of mind, which is relevant for some legal issues but not others. My understanding is that the crux of the case is on the extent to which a number of informal messages should be considered a binding contract.
Trying to go from a single admission like this to an overall legal conclusion is a lot like seeing a single line in a program and then concluding there's a bug -- without having ever seen the rest of the program. You might think "this line always crashes, " but actually it's never called (does not go to any matter at issue), or none of the terms mean what you think they mean, etc.
Pretty surprising -- I had much the opposite experience.
On our last product, we decided to start switching from Typescript to Rust on the backend because we got tired of crashes. I consider that to be one of the greatest technical mistakes I've made ever, as our productivity slowed massively. I'll just share two time-draining issues that only occur in Rust: (1) Writing higher-order functions (e.g.: a function to open a database connection, do something, and then close it -- yes, I know you can use RAII for this particular example), which is trivial in Haskell and TypeScript and JavaScript and C++ and PHP, turned out to be so impossible in Rust [even after asking Rust-expert friends for help], that I learned to just give up and never try, though it sometimes worked to write a macro instead. (2) It's happened many times that I would attempt a refactoring, spend all day fixing type errors, finally get to the top-level file, get a type error that's actually caused somewhere else by basic parts of the design, and conclude the entire refactoring I had attempted is impossible and need to revert everything.
On top of that, Rust is the only modern language I can name where using a value by its interface instead of its concrete type lies somewhere between advanced and impossible, depending on what exactly you're doing.
I came away concluding that application code (as opposed to systems or library code) should, to a first approximation, never be written in Rust.
I'm not a Rust expert by any means, but I'm surprised to hear this. In my Rust code, doing anything with a database connection is not at all different from, say, Go or TypeScript.
For example, I use the deadpool-postgres crate for database pooling. Getting a connection looks like this:
let conn = self.pool.get().await?;
Because of RAII, you don't need a higher-order function helper, but if you really wanted to make one:
with_conn(|conn| async move {
conn.query("SELECT 1") // Or whatever
}).await
If you know TypeScript, this shouldn't be too difficult to read or write. The gnarliest stuff here is knowing the type signature of the function argument; because of async, it must be AsyncFnOnce, for one, and you need to know that the type that the deadpool crate returns is called Object<Manager> (which doesn't sound like a connection, to be fair). Determining the exact concrete type to match type constraints on is sometimes a chore, but TypeScript is surely no different here!
If you don't know Rust too well, the "move" part will be a little mysterious, to be sure.
Just investigated -- looks like this works now! Yay!
For this family of examples, had been completely stymied by AsyncFnOnce not being released yet. IIRC it had been in the works for several years, was still an experimental feature when I was trying to use it, and I gave up after much frustration at trying to get a version of Rust with experimental features working under devenv (nix).
A subtraction then to my frustrations with Rust -- though I'd still be very wary of doing this, having seen how fragile higher-order functions have been in the past.
That explains it. I think async closures were stabilized a year ago. Before that, you'd have needed to write out the async signature as non-async with futures (that's what async is syntactic sugar for, anyway). Something like:
I appreciate Rust for making affine types mainstream, and having at least the C++ community start caring about security, even if half hearted.
However I share your conclusion, outside scenarios where having automated resource management as the main approach is either technically impossible, or a waste of time trying to change pervasive culture, I don't see much need for Rust.
In fact those that write comments about wanting a Rust but without borrow checker, the answer already exists.
I think Rust would be fine for application code if it kept the borrow checker, but had greater allowance for dynamically-sized variables, or even garbage collection. The reason calling things through an interface is so tough in Rust is because doing so requires having a pointer to a value of unknown size, which involves either heap allocation or alloca(), neither of which are very happy in Rust. Many of the other things I complained about are also downstream of this decision. Affine types are useful both in high-level state management as well as in low-level memory management. But it's Rust's focus on static memory layout that really cements it as a low-level systems language, not its inclusion of borrowing.
Way back as an undergrad in 2011, I contributed to Plaid, a JVM language whose main feature is based on affine and linear types. I'm one of the very few people in the world who knew what borrowing is before Rust had it. So I know first-hand that borrow-checking is perfectly compatible with garbage collection.
Exactly, and that is why after Rust's break into mainstream, several garbage collected languages are trying to mix advanced type systems with their approach to garbage collection (GC, RC, a mix of both, whatever).
This is also not strange for those in the Rust community with type systems experience, hence the Roadmap 2026 proposals for a more ergonomic experience.
Thus we have Linear Haskell, Swift 6 ownership, D ownership, Koka, Hylo, Chapel, OxCaml, Scala Capabilities, Ada/SPARK proofs, Idris, F*, Dafny,....
Interesting. But when I search `"roadmap 2026" rust`, I only get results for the video game.
Am familiar with Linear Haskell (and actually went on a walk through Tokyo with one of the authors just a few months ago). IIRC: still no resources allocated to add the things that would actually make it useful. Had not been aware of most of those, except the dependently-typed ones. Cool to know about the others. Yay linear types becoming known.
You seem interesting, and I'm curious about your background now. All I can find so far is that you're from Europe and used to lead C++ in some major corporation.
I wish, I got the luck to do some C++ at CERN and Nokia Networks, but nothing out of the ordinary.
Just happen to be a nerd with interests across systems programming, languages, graphics, that rather reads books and papers than watching dull TV shows.
Day job is boring enterprise consulting across the usual stacks you might imagine.
Have found in that roadmap things that would help me personally like making async functions dyn-compatible. Have not found the deeper stuff I thought you were hinting at.
Last year, I wanted to use the 5- line Result.flatten function, abs then found it had been stuck in experimental....for 5 years. That left me with no confidence of the language's dev velocity.
(1) Higher order functions are pretty much the same as all the other languages you mentioned, using closure syntax? What was the problem you ran into?
(2) In such situations the compiler (type system or borrow checker) is telling you that what you wanted to do has hidden bugs, and therefore refuses to compile. Usually that's a good thing.
(1) Oh sure, the syntax is easy. Getting it to borrow-check is somewhere between insane and impossible. As I said, I've had friends who are actual Rust experts give up trying.
(2) No, it stems from a compiler limitation (imposed in large part by the need for static memory layout), not because there's anything intrinsically buggy about doing this.
(3) Look up "dyn-compatibility", for the largest, but not the only, problem with doing this.
It seems to be a common reflex of Rust advocates that, whenever an issue with using the language is asked about, the response is "That's just a garbage-collected code pattern" followed by "and therefore you shouldn't want it." It's happened multiple times in this thread. [Edit: and both the times I was thinking of were from you, so need to weaken that conclusion]
Aside from having vibes of "I've chosen to get hit weekly in the face with a baseball bat, but have learned to like it, and so should you" it's also seldom true.
All three of these examples are also quite easy to do with C and C++. It's not about garbage collection.
"It's possible to write <lang-X> in <lang-Y>" is a common trope, but "It's possible to write <lang-X> in Rust" is painful and borderline impossible in my experience. I don't mean this as a defense of rust, I just think it's why the learning curve is so harsh.
Rust makes you be explicit about memory management. I guarantee you if you threw everything into a box inside an Arc, your copy closures would have still worked just fine and your Haskell idiom would translate cleanly. Only now everything is heap allocated and reference counted. Before LLMs took over the reins, this was the hallmark of beginner rust code because it WOULD work, just with unnecessary allocations and copying and pointer dereferencing.
Rust makes the tradeoffs explicit, and Rust programmers tend to obsess over minimizing those tradeoffs to get abstractions that are zero-cost. So doing it “the rust way” is often very complicated and tricky to get right while satisfying the borrow checker and type system, but once found is lean, fast, clean, and safe.
Oy. It is also a common experience that, when I struggle in Rust to use a pattern that's common in nearly every other language or find another way to achieve the same goal, people who know of my Haskell background call it a "Haskell pattern," and thereby avoid facing the suggestion that their favorite language is missing some pretty basic affordances.
No, boxing everything does not magically make things more dyn-compatible. It will not magically solve the issue that tokio does a whole-program transformation that does its most restrictive checking only after all local checks have been resolved. It will not magically allow more reuse between datatypes. It will solve none of the problems I encountered... because if beginner-Rust could solve any of these problems, then they would have ceased to be problems for me by the time I became intermediate.
> Rust programmers tend to obsess over minimizing those tradeoffs to get abstractions that are zero-cost. So doing it “the rust way” is often very complicated and tricky to get right while satisfying the borrow checker and type system, but once found is lean, fast, clean, and safe.
You and I must be using very different definitions of "lean." For me, "complicated" and "lean" do not go together
I am sorry that Rust isn't for you. There is beauty in a systems programming language, but you have to be willing to think as a systems programmer. That's not for everybody.
This is getting pretty funny. In this branch of the thread alone, I've seen the defenses of: (1) "Rust is fine, you're just expecting the affordances of a GC language." (2) "Rust is fine, you're just expecting the affordances of Haskell," and now (3) "Rust is fine, you're just not used to systems programming"
It's okay if your language has problems (I have plenty of criticisms of my favorite languages), but I find it odd and concerning how frequently I've seen Rust programmers try to deflect instead of engaging in criticism.
I actually have a huge systems programming background and identify as a systems programmer. C and C++ by and large do not have the problems I've written about. These things are Rust problems, not systems problems.
Maybe it depends on the application, but web servers are effortless with something like axum. Libraries can do a lot of heavy lifting to expose straightforward coding patterns. Never had any problems like you desribed with database connections and such. In rust with db pools things just work and get closed on drop etc. I would never even consider making a higher order function for that.
Only other language that I think gets close to rust ergonomics is Kotlin, but it suffers from having too many possibilities for abstractions.
That's pretty interesting. I was thinking about starting a new pet project and was considering doing it in Rust to learn as I never tried anything with it and after some small pocs I had the feeling it was too verbose to my taste, but wasn't sure it was just me and/or my lack of experience with Rust.
Still, wonder if it's still worth it to give a shot considering other positive elements of the language.
Verbosity aside, whether or not Rust is a good fit depends on what you are doing. The language design is broadly optimized for low-level application code, like command-line utilities. If that is the use case then you are likely to have a good experience.
For high-performance and high-reliability systems code, Rust is much more of a mixed bag. In a systems context it lacks the ability to easily and ergonomically express idiomatic constructs important for safety and performance that are trivial to express in e.g. C++. When you run into these cases it can get pretty ugly.
Most people don't write this kind of systems code. What most people call "systems code" is really more like low-level applications code, where Rust excels. It is software like highly-optimized kernel-bypass database engines and similar where the limitations start to show.
It’s worth learning, in my opinion, but I’ve been writing it professionally for the better part of a decade, so my opinion may be a bit skewed.
It’s my favorite language to write, and it gets much easier over time. As a first approximation, if you’re doing something and it feels insanely difficult like the GP is talking about, try to think of a different way to do it rather than fighting it. There’s usually a way to do almost anything, but it’s more pleasant to lean into the grooves the language pushes you towards.
Rust is definitely very verbose. I think it's a fine choice -- probably even the best choice -- if you're doing systems code or if performance is your most important feature. If not, I would pass.
Some say verbose, some say explicit. I had the complete opposite reaction to Rust than this other person, and I don’t think I’m particularly smart so I don’t think it’s purely a matter of intelligence. Even asynchronous rust is pretty easy once you get the hang of it.
That is a very unusual Rust experience. I find "application code" very pleasant to write in Rust. Of course there are things that aren't as ergonomic in Rust as in other languages (e.g. callbacks) but that's true of pretty much any language.
I have heard this reaction from others before. One of the Rust expert friends I consulted with told me "I'm not convinced you're not trying to write Haskell-style code in Rust;" I told him the patterns I was struggling with were both trivial and common in Java.
The things I found quite difficult or impossible in Rust were to me pretty basic patterns for modularity and removing duplication that it's really shocking that these complaints are not more common.
I currently have but two hypotheses for why.
First, the second problem I mentioned only comes from using tokio, which causes your top-level program to secretly be using a defunctionalized continuation data type, derived from where exactly in other files you put your await's, that might not be Send. If you're not using tokio, you won't experience that issue.
Second...I was kinda told to just give up on deduplication and have lots of copy+pasted code. This raises the very uncomfortable hypothesis that Rust afficionados are some combination of people who came to Rust early and never learned traditional software design and don't know what they're missing, and people who were raised on traditional good software engineering but then got hit with Rust's metaphorical baseball bat of lack-of-modularity over and over until they got used to being hit with a baseball bat as a normal pain of life.
I don't like either of these explanations (esp. with tokio seeming quite dominant), so I'm awaiting an explanation that makes more sense. https://xkcd.com/3210/
> Rust afficionados are some combination of people who came to Rust early and never learned traditional software design and don't know what they're missing
This is definitely not the case and is unnecessarily insulting.
The truth is that some things are harder in Rust but a) often those things are best avoided anyway (e.g. callbacks), and b) it's worth the trade-off because of the other good things it allows.
Surely as a Haskell user of all things you must understand that sometimes making things harder is worth the trade-off. Yeay everything is pure! Great for many reasons. Now how do I add logging to this deeply nested function?
I know that it's insulting! And it doesn't make sense, because I generally think Rust programmers are smart people. But right now, it's the only explanation I've got, so it is alas necessarily insulting. So please, please, please give me a better explanation that actually makes sense.
> The truth is that some things are harder in Rust but a) often those things are best avoided anyway (e.g. callbacks), and b) it's worth the trade-off because of the other good things it allows.
This sounds like the seeds of a better explanation, but it needs a lot more to actually suffice. E.g.: why are callbacks best avoided anyway, when they're virtually required for a large number of important programming patterns? (In more technical language: they're effectively the only way to eliminate duplication in non-leaf-expressions. In even more technical language: they're the way to do second-order anti-unification.)
> Surely as a Haskell user of all things you must understand that sometimes making things harder is worth the trade-off. Yeay everything is pure! Great for many reasons. Now how do I add logging to this deeply nested function?
And this is a great illustration of the difference. First, you will seldom find Haskell programmers trying to argue that, actually, things like deeply-nested logging that everyone wants are actually "best avoided anyway." Second, you'll actually get a solution if you ask about them -- in this case, to either use MTL-style, to use a fixed alias for your monad stack, or that unsafePerformIO isn't actually that bad.
BTW, similar to my unpleasant conclusion for Rust above, I have another unpleasant conclusion for Haskell: Haskell is incredible for medium-sized programs, but it has its own missing modularity features that make it non-ideal for large programs (e.g.: >50k lines). But this is a much smaller problem than it sounds because Haskell is so compact that, while many projects can be huge, very few individual codebases will need to approach that size.
Look up "callback hell". Basically they encourage spaghetti.
> you'll actually get a solution if you ask about them
You got solutions to your problems didn't you? Macros are a perfectly reasonable thing to use in Rust, even if they are best avoided where possible. Exactly like unsafePerformIO.
If you were expecting Rust to work perfectly in every situation... well it doesn't. GUI programming in particular is still awkward, and async Rust has more footguns than anyone is happy with.
Despite that it's still probably the best language we have for a surprisingly large range of domains.
> Look up "callback hell". Basically they encourage spaghetti.
Ah. I think you're confusing the general idea of a callback with one particular style of use. "callback hell" refers to the deep indentation that occurs when trying to program in monadic style in languages without syntactic support for monads. It was mostly solved by adding async/await syntax, aka syntactic support for the continuation monad. "Callback hell" is not spaghetti in any deep sense, merely syntactically cumbersome.
But a "callback" is a more general term, sometimes a synonym for "function parameter," sometimes for more narrow kinds of function parameter (e.g.: void function, invokable once). Many people will refer to the function argument of the `map` function as a callback, but no-one would refer to that as "callback hell."
And when I did, I largely got it by figuring stuff out myself, while being told by multiple Rust experts that I either shouldn't care about the verbosity and lack of modularity, or that if I have a problem like "using the interface instead of the implementation" it must be because I'm a Haskeller.
Well, my ultimate solution was to start working on a new product, and to not use any Rust, except for some performance-heavy libraries. With the first product, the market had changed too much by the time we were ready for prime-time, and I'd put somewhere between 25% and 70% of the reason for that delay on our choice to start building new parts of the backend in Rust.
> Macros are a perfectly reasonable thing to use in Rust, even if they are best avoided where possible. Exactly like unsafePerformIO.
Good comparison!
> Despite that it's still probably the best language we have for a surprisingly large range of domains.
I agree with this. I just don't agree that that list of domains has a very large intersection with the set of applications.
> difficult or impossible in Rust were to me pretty basic patterns for modularity
Many things are plainly not permitted, either because the borrow-checker isn't clever enough, or the pattern is unsafe (without garbage collection and so on).
Many functional/Haskell patterns simply can not be translated directly to Rust.
That "and so on" is doing a lot of work. You may accept rejecting garbage collection as a reasonable trade-off, but the bulk of the cost is coming from a much more aggressive tradeoff Rust is making with is at odds with the goals of most application code.
A deeply-baked assumption of Rust is that your memory layout is static. Dynamic memory layout is perfectly compatible with manual memory management, but Rust does not readily support it because of its demands for static memory layout.
A very easy place to see this is the difference in decorator types between Rust and other languages like Java. Java's legacy File/reader API has you write things like `new PrintWriter(new BufferedWriter(new FileWriter("foo.txt")))`, where each layer adds some functionality to the base layer. The resulting value has principal type `PrintWriter` and can be used through the `Writer` interface.
The equivalent code in Rust would give you a value of type `PrintWriter<BufferedWriter<FileWriter>>` which can only be passed to functions that expect exactly that type and not, say, a `PrintWriter<BufferedWriter<StringStream>>`. You would solve this by using a template function that takes a `T where T: Writer` parameter and gets compiled separately for every use-site, thus contributing to Rust's infamous slow build times.
It would be perfectly sane, and desirable for application code, to be able to pass around a PrintWriter value as an owned pointer to a PrintWriter struct which contains an owned pointer to a BufferedWriter struct which contains an owned pointer to a FileWriter struct. You could even have each pointer actually be to a Writer value of unknown size, and thus recover modularity.
In Rust, there is sometimes a painful and very fragile way to do this: have each writer type contain a Box<&dyn Writer>, effectively the same as the Java solution above. This works, except that, if one day you want to add a method to the Writer trait that breaks dyn-compatibility, then you will no longer be able to do this, and will need to rewrite all code that uses this type.
You can usually manage dyn compatibility issues in my experience by writing a base trait that is not dyn compatible and then an Ext trait that is, which is auto implemented for all implementers of the base trait. You see this pattern all over the place, including with several of the buffer traits you mentioned.
Mostly, this works out well enough: dyn compatibility pretty much just insists your methods can in fact work with just a reference to an unknown variant of the type.
Good suggestion. I think started doing that kind of thing towards the end of my days with Rust. It's been close to a year now, and don't remember how well it worked out.
Some people ask me why I do not use Rust as opposed to C++ if it is already safer and more modern.
But I see the forums (and I also trued some toy stuff at times) plagued with rigidity problems that in C++ have obvious solutions.
For example, I am not going to fight a borrow-checker all the stack up to get a 0.0005% perf improvement, if sny, when I can use smart pointers.
I am not going to use Result everywhere when I can throw an exception and get done with it instead of refactoring all the stack up for the intermediate return types (though I use expected and optional and like them, but it is a choice depending on what I am doing).
I am not going to elaborate safe interfaces for my arrays of data I need to send to a GPU: there is no vslue in it and I can get it wrong snyway, it os ceremony. I assume this kind of code is unsafe by nature.
I find C++ just more flexible. Yes, it has warts, but I use all warnings as errors, clang tidy and have a lot of flexibility. I use values to avoid any trace of dangling and when it is going to get bad, I can, most of the time, switch to smart pointers.
I really do not get why someone would use Rust except for very niche cases like absolutely no memory unsafety (but this is not free either, as some reports show: you need to really be careful about reviewing unsafe if your domain is unsafe by nature or uses bindings to keep Rust invariants or you write only safe code, in whcih case, if memory safety is critical, it does give you something).
But I do not see Rust good for writing general application code. At least not compared to well-written C++ nowadays.
“I’m not going to use Rust because I don’t like it” seems like what you’re saying, which is totally fine. Plenty of people, myself included, manage to write and enjoy writing general application code in Rust. You’re allowed to not get it, just like I’m allowed to dislike writing C++.
No. That is not what I am saying. I am saying there are contexts where you do not get value out of it and you can potentially decrease your productivity because it is more rigid. You have examples above if you want to read through.
In no way I am saying it is useless. I just see niche uses for it compared to alternatives.
I read most of your comment as phrasing the things that make rust unique as being additional burdens relative to what you would prefer, which is fair, but often they are what I appreciate about the language. Explicit result types are a great example.
Rigidity is a trade off: it can make initial development slower but refactors significantly easier, just as an example.
I don’t think any of your examples show it to be niche. It operates well in most of the space where C++ is a good option, and a bit beyond that (embedded, firmware, but also higher level things where you want performance but don’t want to worry about memory safety).
> but also higher level things where you want performance but don’t want to worry about memory safety).
Well, at the cost of having a straight jacket. Result without option for exception handling is an example. You need to refactor all the way up if you notice that suddenly when refactoring you needed a Result bc a new error appears that could not happen before or you need to preventively spam Result everywhere since the start. You need to handle those all the stack up. The borrow checker is also rigid. I do know why it exists. I understand its value. I am just talking about the toll it imposes while coding, and wondering if it is a good default (I think most of the time it is not, but when you need it, it is invaluable, however these cases are a minority).
Another insight is that when you really go low-level, most of the time you are working with unsafe interfaces probably. At that time, you are using unsafe and now you have to satisfy Rust's borrow checker. How? By hand. So you lost part of the value proposition.
Can you recover it? Yes. How? By reviewing that code. But if I have to review that code, what is better from choosing a language (in this situation I mean, there are situations where Rust is the better choice) where I can understand the invariants in unsafe code better and anyway I have linters and a lot of established guidelines that are not difficult to follow? And by not difficult to follow I mean they are embedded in tooling like clang-tidy, not that I can follow because I know a lot.
So for me it is not so obvious at all, especially in the presence of quite a few unsafe blocks. If you want it safe, at that time, you are starting to compete with other unsafe languages: you need human review anyways... if there is tooling in Rust for unsafe blocks (I can imagine there could be something), that improves things competitively for Rust in unsafe blocks. But if you need careful review, you are stuck again in the non-magical real world: things are safe if you checked absolutely everything.
> Rigidity is a trade off: it can make initial development slower but refactors significantly easier, just as an example.
That is certainly true. It is also true that in areas where you put this extra effort and quickly refactor, it makes things more difficult.
Refactoring, if you mix it with unsafe, needs a much more strict review than just pretending things are safe because you refactored and put things behind an unsafe interface and present it as safe.
I am not convinced at all this is what you need in most scenarios. The productivity impact is relatively high IMHO.
OTOH, if I really want correctness (real correctness!) but not absolute full speed, I think I can reach to Ocaml (very practical) or Haskell (this one is also a bit too rigid actually sometimes).
So I am left in a situation where Rust just seems to be appealing for places where the most absolute memory safety is needed. But memory safety is still a composed characteristic of a running program: you have to take into account unsafe interfaces, bindings, etc.
So the only way to get real safety is anyways to review everything (if that is what you really want to deliver), probably proving your code, which anyway requires human intervention. Did we ever (even if less often) see crashes for invariant violations in code advertised as safe in Rust? Certainly yes. I acknowledge it is usually an improvement, but still not a guarantee.
So if it is not a guarantee and I can reach other tools where anyway the guarantee is there through GC or other mechanisms and where it is not I am equal to Rust, then, why bother?
Probably the only place where I see Rust appealing is where you need both max. performance and absolute memory safety (but you will still need the kind of reviews I mentioned if you spam unsafe and interact with bindings anyway). Those are niche cases, not the norm.
I see like a suboptimal choice to write much of the application code in Rust, even when you need speed, compared to C++. C++ has very good tools for compile-time programming, expression templates, good warnings and linters, a big ecosystem and it is way more voluble (exceptions and results can be used, invariants in unsafe code are easier to follow since a borrow checker does not need to be satisfied "by hand").
So I am not sure at all Rust is the reply for a more or less mainstream general-purpose application language.
There is no magic bullet here, but I do know that when coding in Rust, the productivity toll I am paying is not negligible and I can reach for tools and techniques that make me very close or equal to that productivity.
*`dyn Writer`. `impl Writer` can only be used in function parameters.
This was one of the example approaches I gave. This works...at first. The problem is that, if you want to add a new function to the Writer trait which makes Writer no longer dyn-compatible, such as, say, any async function, then you can no longer write `Box<dyn Writer>` and need to rewrite all code that uses it.
(although you can dig under the hood and specify a pinned-down Future type, covering one kind of awfulness with another)
The most confusing thing that can happen with something like tokio is the failure-at-distance you can get from writing a non-Send future somewhere in the depths of a call stack and then having to figure out why your top-level spawn isn’t working. There’s a non-default lint I highly recommend turning on when working with tokio: clippy::future_not_send. Forces all your futures to be Send, unless you opt out, which really helps keep the reasoning local when you run into errors.
FWIW I write primarily rust, and I do not agree with the advice given in your second point, so I’d take it with a grain of salt were I you.
> Rust afficionados are some combination of people who came to Rust early and never learned traditional software design and don't know what they're missing, and people who were raised on traditional good software engineering but then got hit with Rust's metaphorical baseball bat of lack-of-modularity over and over until they got used to being hit with a baseball bat as a normal pain of life.
Oh please. I came to Rust late, after using plenty of other languages. I have never had a problem with “modularity”. You express surprise that these issues haven’t been talked about more, well the null hypothesis is that you are just not very good at Rust and it hasn’t clicked for you - that’s fine, I doubt I would be very good at Haskell. But don’t insult us, and don’t assume your experience is de facto everyone’s and we’re just in denial. It’s incredibly arrogant.
Please respect the fact that I do not understand how one can like Rust without giving up caring about modularity.
I have pretty good reason for thinking this is a possibility. Directly, because the Rust experts I asked for help did in fact advocate giving up modularity. Indirectly, in that I've seen something similar but much stronger in a different language. That culminated in a conversation with several of the creators of that language, in which they argued their language was great for generic programming...while revealing some pretty basic holes in their understanding of generic programming (while calling me a "Haskell weenie," among other abuse). I am very confident in my conclusion about this other language community and not interested in getting into the details of this event. I am just sharing where I'm coming from, in not immediately dismissing this idea as too absurd to consider even when I have no other hypothesis.
I acknowledge that you feel personally insulted by me having a hypothesis that requires a large group of people behave in ways I consider strange. I have evidence for that hypothesis, and have been unable to find a better one. I hope you can see how the comment I am responding to crosses an extra line and is directly personally insulting.
I would very much like to come away from this discussion no longer believing that Rust programminh is at odds with modularity. I have shared some fairly basic and detailed criticisms, the ones I still remember after a year out of the language. Perhaps your can play a role in offering solutions to them -- or admit that these are indeed problems your hadn't noticed before or had gotten used to
No-one comes out of the womb caring about code quality. People learn to care about the craft precisely because internal quality -- cohesion, modularity, robustness -- leads to external quality (correctness, speed, evolvability).
People who care about code quality are not artists who want to paint on the company's dime. They are people who care about shipping a product deeply enough to make sure that doing so is a pleasant experience both for themselves and their colleagues, and also have the maturity to do a little bit more thinking today, so that next week they can make better decisions without thinking, so that they don't get called at 4 AM the night after launch for some emergency debugging of an issue that that really should have been impossible if it was properly designed.
> No one has ever made a purchasing decision based on how good your code is.
Usually they don't get to see the internals of the product, but they can make inferences based on its externals. You've heard plenty of products called a "vibe-coded piece of crap" this year, even if they're not open source.
But also, this is just not true. Code quality is a factor in lots of purchasing decisions.
When buying open source products, having your own team check out the repo is incredibly common. If there are glaring signs in the first 5 minutes that it was hacked together, your chances of getting the sale have gone way down. In the largest deals, inspecting the source code
It was for an investment decision rather than for a purchase, but I've been personally hired to do some "emergency API design" so a company can show that it both has the thing being designed, and that their design is good.
> People who care about code quality are not artists who want to paint on the company's dime. They are people who care about shipping a product deeply enough to make sure that doing so is a pleasant experience both for themselves and their colleagues, and also have the maturity to do a little bit more thinking today, so that next week they can make better decisions without thinking, so that they don't get called at 4 AM the night after launch for some emergency debugging of an issue that that really should have been impossible if it was properly designed.
Speak for yourself. This is exactly the GPs point. Some people care more about the craft of code than the output. I personally find writing good code to be what motivates me. Obviously its a spectrum; shipping is good too. But it's not why I get up in the morning.
Okay, I admit I went too hard on that one to fight against the OP. I too get extreme pleasure from condensing a 300-line function into 30 lines of simplicity, even when practical considerations (it has a clean interface and no-one's even needed to open this file in 2 years) would dictate elegance there is not helpful.
Code quality is a side-effect of caring. The most important part of product design is caring at all levels. However it's caring about the external details that is the most important. Coding language is largely a function of the population of good coders in your areas. Code evolvability is almost entirely subjective.
Let's take two code bases. A 40 year old legacy Cobol monstrosity being maintained by a staff of well trained engineers that are engaged. The second an idiomatic Haskell application written simply and to the highest standards, but with a staff of junior engineers that specialize in imperative programming. The Cobol code base is much more evolvable. The staff can effectively make changes because they deeply know the product.
Evolvability is not an intrinsic feature of the software. It is a synthesis of culture, talent, passion, and code style.
I see where you're coming from. I totally agree that how easy it is for a company to change a codebase depends on the talent it has. But i think you can separate the entire codebase+ecosystem+team codebase from the quality of the codebase itself.
To use a simple analogy: you could argue that a modern Macbook is easier to maintain than an old Dell desktop, if the Macbook is owned by a certified Apple technician who knows how to microsolder, and the old Dell desktop is owned by someone who doesn't know how to use a screwdriver. But that doesn't change that, to change a hard drive, one needs much more skill and fancier tools than the other.
Timely! I spent a good chunk of yesterday investigating Bombadil. I'm a huge fan of PBT and a huge fan of temporal logic (though not linear temporal logic) and we're currently trying to level up our QA, so it seemed like a good match.
Unfortunately, I concluded that Bombadil is a toy. Not as in "Does some very nice things, but missing the features for enterprise adoption." I mean that in a very strong sense, as in: I could not figure out how to do anything real with it.
The place where this is most obvious is in its action generator type. You give it a function that generates actions, where each action is drawn from a small menu of choices like "scroll up this many pixel" or "click on this pixel." You cannot add new types of actions. If you want to click on a button, you need it to generate a sequence of actions to first scroll down enough, and then look up a pixel that's on that button.
Except that it selects actions randomly from your list, so you somehow need the action generator, when run the first time, to generate only the scroll action, and then have it, when run the second time, generate only the click action. If you are silly enough as to have an action generator that, you know, actually gives a list of reasonable actions, you'll get a tester that spends all its time going in circles clicking on the nav bar.
(Something in the docs claimed that actions are weighted, but I found nothing about an actual affordance for doing that. Having weights would make this go from basically impossible to somewhat impossible.)
(Edit: Found the weighted thing.)
I am terrified to imagine how to get Bombadil to fill out a form. At least that seems possible -- you can inspect the state of the web page to figure out what's already been filled out. But if you want the state to be based on anything not on the current page, like the action that you took on the previous page, or gasp the state on disk (as for an Electron app), that seems completely impossible. Action generators are based on the current state, and the state must be a pure function of the web page.
Its temporal logic has a cool time-bounded construct, but it's missing the U (until) operator. One of their few examples is "If you show the user a notification, it disappears within 5 seconds." But I want to say "When you click the Generate button, it says 'Generating...' up until it's finished generating." And I can't.
(Note: everything above is according to the docs. Hopefully everything I said is a limitation of the docs, not an actual limitation of the framework.)
I shared my comments with the author yesterday on LinkedIn, but he hasn't responded yet. Maybe I'll hear from him here.
I have a pretty positive opinion of Antithesis as a company and they seem to be investing seriously in it, and generally see it as a strong positive sign when someone knows what temporal logic is, so I have hopes for this framework. I am nonetheless disappointed that I can't use it now, especially because I was supposed to finish an internal GUI testing tool this week and my god I'm behind.
Hey, yeah, so actions are indeed independent and you can't express fixed sequences of them (e.g. fill out thus form by doing X, then Y, then Z). If you want to enforce certain sequentiality you'd need to precondition your generators. This is admittedly limited right now. I do want to add QoL things like `filter` on the generators to make that more ergonomic.
Also, as you note, you can't implement custom actions. And that's just something that I haven't gotten to yet. It'd be quite straightforward to add a plain function (toStringed) as one of the variants of the Action type and send that to the browser for eval. (Btw, we're taking contributions!)
Weighting is also important right now. There's no smart exploration in Bombadil yet, only blind random, so manual weighting becomes pretty crucial to have more effective tests (i.e. unlikely to run in circles). I'd like to both make the Bombadil "fuzzer" better at this, but eventually you might want to run Bombadil inside Antithesis instead to get a much better exploration, even in a single campaign.
The Until operator is also one of those things that I haven't gotten to yet. I actually didn't expect someone to hit this as a major limitation of the tool so quickly, which is why I've focused on other things. Surprising!
To add some more context, Bombadil is an OSS project with one developer (but we're hiring!) and it's 4 months old and marked experimental. I'm sorry you were disappointed by its current state, but it's very early days, so expect a lot of these things to improve. And your feedback will be taken into account. Thanks!
While the title sounds intriguing, I'd really like examples to explain what this is good for. The Github page has more information, but it's not helpful. "XML instead of JSON" -- okay, why? I thought the consensus was generally that JSON is a better data format than XML thanks to its compactness and simplicity.
Doug has not signed up for it.
reply