The Cargo example at the top is striking. Whenever I publish a crate, and it blocks me until I write `--allow-dirty`, I am reminded that there is a conflation between Cargo/crates.io and Git that should not exist. I will write `--allow-dirty` because I think these are two separate functionalities that should not be coupled. Crates.io should not know about or care about my project's Git usage or lack thereof.
> The Cargo example at the top is striking. Whenever I publish a crate, and it blocks me until I write `--allow-dirty`, I am reminded that there is a conflation between Cargo/crates.io and Git that should not exist. I will write `--allow-dirty` because I think these are two separate functionalities that should not be coupled.
That's completely unrelated.
The --allow-dirty flag is to bypass a local safety check which prevents you from accidentally publishing a crate with changes which haven't been committed to your local git repository. It has no relation at all to the use of git for the index of packages.
> Crates.io should not know about or care about my project's Git usage or lack thereof.
There are good reasons to know or care. The first one, is to provide a link from the crates.io page to your canonical version control repository. The second one, is to add a file containing the original commit identifier (commit hash in case of git) which was used to generate the package, to simplify auditing that the contents of the package match what's on the version control repository (to help defend against supply chain attacks). Both are optional.
Those are great points, and reinforce the concept that there is conflation between Cargo and Git/commits. Commits and Cargo IMO should be separate concepts. Cargo should not be checking my Git history prior to publishing.
Indeed. I believe the parent commenter should pause and assess why people buy things, and what makes something desirable or a luxury good. We can start with mechanical watches: They are of poorer quality by every utilitarian measure than a crystal-oscillator driven one, but command much higher prices and status.
> They are of poorer quality by every utilitarian measure than a crystal-oscillator driven one, but command much higher prices and status.
But the value of the watch is not reducible to its value as a timekeeping device. (Even here, you could argue that the mechanical watch has more instrumental value as a timekeeping device where batteries are unavailable.)
A mechanical watch has greater value as a mechanical watch; it is mechanically more sophisticated, even if not electronically. It can have greater value as a product of superb craftsmanship or as an object of art. (And here, while tastes vary, I would reject the reduction of beauty to taste.)
> it is mechanically more sophisticated, even if not electronically. It can have greater value as a product of superb craftsmanship or as an object of art.
On any objective measurement axis a $15 Casio is more sophisticated than a $10_000 Rolex. I think what we value is the human scale of the Rolex, it operates and is manufactured at a scale we intuitively understand as humans, and we(or at least some people) value the sacrifices and effort needed to run at that scale.
Consider this, on your cheap Casio, the manufacturing tolerances are so tight and the parts are so complex and fine the only way to manufacture them are fully automated lines requiring a staggering capital investment of many millions, however because these lines have to be fully automated the economy of scale applies hard and the final product is very inexpensive.
All it takes to make a fine mechanical watch is a good watchmaker and several hundred thousand dollars of tooling.
One of my favorite watch repair videos is of a guy who rescues a smashed Casio, It has this fun combination of. it's a Casio, not worth even looking at. It is not designed to be serviced. Everything in it is super tiny, I mean watchmaking is already an exercise in frustration with how small everything is, which is why I enjoy watching them work but I have no real desire to do it myself, however in this Casio they were absurdly small. But this madlad did it. What a heroic fix.
There are various kinds of value and it is a mistake to confuse them or to reduce them to just one kind.
W.r.t. your imputed definition, there is a sense in which everything is irreplaceable, because the identity of two things that are otherwise entirely identical is never the same; that X is never this X.
But I was not making the claim that the value of something is its “ability be itself”. I was claiming that a thing x of kind A is more valuable as an A than a thing y of kind B as an A, where A is not B. This is trivially true.
Consider the utilitarian value of a pencil as an object for writing and consider a particle accelerator. It should be clear that the utility of a pencil as a writing instrument is greater than the utility of a particle accelerator as a writing instrument. The particle accelerator is more complex as a piece of technology, but so what? It has no value as an instrument for writing down your grocery list.
In any case, I was not proposing a comprehensive basis for a theory of value on this notion alone, so I’m not sure how you managed to selectively read that into my comment. It was only listed as one way in which one could say that a mechanical watch is more valuable than a Casio.
That is true, but that doesn't mean Nvidia is not engaging in engineering to intentionally kneecap competition. Triton and other languages like that are a huge threat and CUtile is a means to combat that threat and prevent a hardware abstraction layer.
Hundreds of thousands of developers with access to a global communication network were not stopped by AMD. Why act like dependents or wait for some bright star of consensus unless the intent is really about getting the work for free?
We don't have to wait for singular companies or foundations to fix ecosystem problems. Only the means of coordination are needed. https://prizeforge.com isn't there yet, but it is already capable of bootstrapping its own development. Matching funds, joining the team, or contributing on MuTate will all make the ball pick up speed faster.
>We don't have to wait for singular companies or foundations to fix ecosystem problems.
Geohot has been working on this for about a year, and every roadblock he's encountered he has had to damn near pester Lisa Su about getting drivers fixed. If you want the CUDA replacement that would work on AMD, you need to wait on AMD. If there is a bug in the AMD microcode, you are effectively "stopped by AMD".
We have to platform and organize people, not rely on lone individuals. If there is a deep well of aligned interest, that interest needs a way to represent itself so that AMD has something to talk to, on a similar footing as a B2B relationship. When you work with other companies with hundreds and thousands of employees, it's natural that emails from individuals get drowned out or misunderstood as circulated around.
You can see in his table he calls out his AMD system as having "Good" GPU support, vs. "Great" for nvidia. So, yes, I would argue he is doing the work to platform and organize people, on a professional level to sell AMD systems in a sustainable manner - everything you claim that needs to be done and he is still bottlenecked by AMD.
A single early-stage company is not ecosystem-scale organization. It is instead the legacy benchmark to beat. This is what we do today because the best tools in our toolbox are a corporation or a foundation.
Whether AMD stands to benefit from doing more or less, we are likely in agreement that Tinygrad is a small fraction of the exposed interest and that if AMD were in conversation with a more organized, larger fraction of that interest, that AMD would do more.
I'm not defending AMD doing less. I am insisting that ecosystems can do more and that the only reason they don't is because we didn't properly analyze the problems or develop the tools.
Element web and PC applications are still, in 2025, a mess. I have heard you have to use it on Mobile using the ElementX.
No new complaints: The standard it badgers you to authenticate, then doesn't let you due to errors. Slow to load messages, inconsistent whether edits will show or not, inits channels to an arbitrary time in the past, then you have to click the arrow a few times and wait to get to the latest, the page won't load on mobile, etc.
Yup, work has been going into refactoring Element Web into MVVM components so we can switch out the ancient matrix-js-sdk underpinnings with the same matrix-rust-sdk that is a day-and-night improvement. https://element.io/blog/element-x-web-a-glimpse-into-the-fut... gives an idea.
I don't know if this is still true, or related, but that area used to be (Circa 10-30 years ago) very highly prone to power outages. The reason was lots of old trees near the lines that would inevitably fall; blackouts in local areas were common due to this.
That's an interesting data point, but I don't think it's relevant. The datacenters themselves are designed with a high level of power reliability and can island themselves if needed.
Very cool on on the business model details! I love this model of doing it for multiple reasons.
One thing I am confused on that is tangential to the main topic: What does this (SAAS?) service do? It looks like it might be a middle ground between Heroku and Wordpress? A GUI website builder of some sort with an integrated database, and tool for editing articles or other content with a web UI?
It’s a headless CMS. One place where editors can store and edit content, which is then exposed through a REST API so you can use it in your website, app, emails, etc…
Huge companies use it to centralise marketing copy and media.
Not just huge companies.. lots of web agencies [1] and mid-sized businesses use us to manage their web presence, mostly for the same reason: building custom sites quickly without the hassle of maintaining software. We’re not really optimized for huge websites (or customers).
Yea... I don't trust the motivations, but can confirm that on AA radars looking low (Where you might find UAS or just low-flying aircraft), wind farms show up as clusters of false hits.
Yea; it will be obvious if you've accidentally locked into one, then look at it with eyes or other equipment. And the 0 ground speed. But UAS could hide in them effectively I speculate?
It is more difficult than you may be assuming. How do you know the hits are false? These "hits" are collections of samples at points in time, not continuous tracks. The "tracks" are reconstructed by making inferences from the samples.
Determining whether any pair of sequential samples represents the same entity or two unrelated entities is an extremely difficult inference problem with no closed or general solution. If there is too much clutter, it becomes almost unresolvable. Aliasing will create a lot of false tracks.
History has shown that any heuristic you use to filter the clutter will be used by your adversary as an objective function to hide from your sensors once they know you are using it (e.g. doppler radar "notching").
For this reason the inference algorithms are classified but they will degrade rapidly with sufficient clutter no matter how clever. It is a limitation of the underlying mathematics.
I have what I thought was a broad knowledge base of rust an experience in it over many domains, but I haven't heard of most of those. Have been getting by with `&`, and `&mut` only from those tables!
Incidentally, I think this is one of Rust's best features, and I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!
Incidentally, I recently posted in another thread here how I just discovered the 'named loop/scope feature, and how I thought it was great, but took a while to discover. A reply was along the effect of "That's not new; it's a common feature". Maybe I don't really know rust, but a dialect of it...
> Incidentally, I recently posted in another thread here how I just discovered the 'named loop/scope feature, and how I thought it was great, but took a while to discover. A reply was along the effect of "That's not new; it's a common feature". Maybe I don't really know rust, but a dialect of it...
I assume I'm the one who taught you this, and for the edification of others, you can do labeled break not only in Rust, but also C#, Java, and JavaScript. An even more powerful version of function-local labels and break/continue/goto is available in Go (yes, in Go!), and a yet more powerful version is in C and C++.
The point being, the existence of obscure features does not a large or complex language make, unless you're willing to call Go a large and complex language. By this metric, anyone who's never used a goto in Go is using a dialect of Go, which would be silly; just because you've never had cause to use a feature of a language does not a dialect make.
Coming from 14 years of Perl, and dabbling in Perl 6, I don’t consider Rust a “large language”… but like Perl (and to an extent C++) I do find people craft their own dialects over time via osmosis.
And I don’t see anything bad about this!
After 11 years of full-time Rust, I have never needed to use Pin once, and it’s only having to do FFI have I even had to reach for unsafe.
Unless you memorise the Rust Reference Manual and constantly level up with each release, you’ll never “know” the whole language… but IMHO this shouldn’t stop you from enjoying your small self-dialect - TMTOWTDI!
As someone else who has learned (and forgotten) a great deal of Perl and C++ arcana: The badness is that it makes it harder for one person to understand another person's code.
It's a terrible feature, really. If you need a labeled break what you really need is more decomposition. I'm pretty sure that Dijkstra would have written a nice article about it, alas, he is no longer with us.
I doubt he would, goto heavy code really jumped all over the place in incomprehensible manners. Labelled break/goto is used in perhaps 1% of loops due to actually implementing some algorithm that would've needed extra flags,etc. and EVEN THEN don't break the scoped readability.
There's a huge difference between reining in real world chaos vs theoretical inelagancies (ESPECIALLY if fixing that would introduce other complexity to work around the lack of it).
Dijkstra was wrong on this one. Breaks and continues help to keep the code more readable by reducing the amount of state that needs to be tracked.
Yes, from the purely theoretical standpoint, you can always rewrite the code to use flags inside the loop conditions. And it even allows formal analysis by treating this condition as a loop invariant.
But that's in theory. And we all know the difference between the theory and practice.
Dijkstra did not say anything about "break" and "continue".
Moreover, "continue" has been invented only in 1974, as one of the few novel features of the C programming language, some years after the Dijkstra discussion.
Both simple "break" and "continue" are useful, because unlike "goto" they do not need labels, yet the flow of control caused by them is obvious.
Some languages have versions of "break" and "continue" that can break or continue multiple nested loops. In my opinion, unlike simple "break" and "continue" the multi-loop "break" and "continue" are worse than a restricted "goto" instruction. The reason is that they must use labels, exactly like "goto", but the labels are put at the beginning of a loop, far away from its end , which makes difficult to follow the flow of control caused by them, as the programmer must find first where the loop begins, then search for its end. Therefore they are definitely worse than a "goto".
Instead of having multi-loop break and continue, it is better to have a restricted "goto", which is also useful for handling errors. Restricted "goto" means that it can jump only forwards, not backwards, and that it can only exit from blocks, not enter inside blocks.
These restrictions eliminate the problems caused by "goto" in the ancient non-structured programming languages, which were rightly criticized by Dijkstra.
Many of the things like "&own" are ideas being discussed, they don't exist in the language yet. As far as I know only &, &mut and raw pointers (mut and const) exist in stable rust at this point. The standard library has some additional things like NonNull, Rc, etc.
I doubt that anybody truly knows Rust. And this is aggravated by the fact that features keep getting added. But here are two simple strategies that I found very effective in keeping us ahead of the curve.
1. Always keep the language reference with you. It's absolutely not a replacement for a good introductory textbook. But it's an unusually effective resource for anybody who has crossed that milestone. It's very effective in spontaneously uncovering new language features and in refining your understanding of the language semantics.
What we need to do with it is to refer it occasionally for even constructs that you're familiar with - for loops, for example. I wish that it was available as auto popups in code editors.
2. Use clippy, the linter. I don't have much to add here. Your code will work without it. But for some reason, clippy is an impeccable tutor into idiomatic Rust coding. And you get the advantage of the fact that it stays in sync with the latest language features. So it's yet another way to keep yourself automatically updated with the language features.
I feel like other languages also have the issue of complexity and changing over time. I doubt I know all of C++ post C++14 for example (even though that is my day job). Keeping up with all the things they throw into the standard library of Python is also near impossible unless you write python every day.
Rust has an unusually short release cycle, but each release tends to have fewer things in it. So that is probably about the same when it comes to new features per year in Python or C++.
But sure, C moves slower (and is smaller to begin with). If that is what you want to compare against. But all the languages I work with on a daily basis (C++, Python and Rust) are sprawling.
I don't have enough experience to speak about other languages in depth, but as I understand it Haskell for example has a lot of extensions. And the typescript/node ecosystem seems to move crazy fast and require a ton of different moving pieces to get anything done (especially when it comes to the build system with bundlers, minifiers and what not).
Languages should be small, not large. I find that every language I've ever used that tries to throw everything and the kitchensink at you eventually deteriorates into a mess that spills over into the projects based on that language in terms of long term instability. You should be able to take a 10 year old codebase, compile it and run it. Backwards compatibility is an absolute non-negotiable for programming languages and if you disagree with that you are building toys, not production grade systems.
I'm not sure what this is arguing against here. Anyone who follows Rust knows that it's relatively modest when it comes to adding new features; most of the "features" that get added to Rust are either new stdlib APIs or just streamlining existing features so that they're less restrictive/easier to use. And Rust has a fantastic backwards compatibility story.
I had C++, python and ruby in mind, but yes, GP also mentioned Rust in the list of 'sprawling' languages, and they are probably right about that: Rust started as a 'better C replacement' but now it is trying to dominate every space for every programming language (and - in my opinion - not being very successful because niche languages exist for a reason, it is much easier to specialize than to generalize).
I wasn't particularly commenting on Rust's backward compatibility story so if you're not sure what I was arguing about then why did you feel the need to defend Rust from accusations that weren't made in the first place?
Egad, no. This is how you get C++, whose core tenet seems to be “someone used this once in 1994 so we can never change it”.
Even adding a new keyword will break some code out there that used that as a variable name or something. Perfect backward compatibility means you can never improve anything, ever, lest it causes someone a nonzero amount of porting effort.
No, you get C++ because you're Bjarne Stroustrup and trying to get people to sign on to the C++ bandwagon (A better C! Where have I heard that before?) and so you add every feature they ask for in the hope that that will drive adoption. And you call it object oriented (even if it really isn't) because that's the buzz-word du-jour. Just like Async today.
I’ll accept that, too. But from the outside it seems like they do that by finding bizarre, previously invalid syntax and making that the way to spell the new feature. `foo[]#>££{23}` to say “use implicit parallelism on big endian machines with non-power-of-2 word sizes”? Let’s do it!
Yes. At the very least, features should carry a lot of weight and be orthogonal to other features. When I was young I used to pride myself on knowing all the ins and outs of modern C++, but over time I realized that needing to be a “language lawyer” was a design shortcoming.
All that being said I’ve never seen the functionality of Rust’s borrow checker reduced to a simpler set of orthogonal features and it’s not clear that’s even possible.
If you want or have to build a large program, something must be large, be it the language, its standard library, third party code, or code you write.
I think it’s best if it is one of the first two, as that makes it easier to add third party code to your code, and will require less effort to bring newcomers up to speed w.r.t. the code. As an example, take strings. C doesn’t really have them as a basic type, so third party libraries all invent their own, requiring those using them to add glue code.
That’s why standard libraries and, to a lesser extent, languages, tend to grow.
Ideally that’s with backwards compatibility, but there’s a tension between moving fast and not making mistakes, so sometimes, errors are made, and APIs ‘have’ to be deprecated or removed.
It's a balance thing. You can't make a language without any features, but you can be too small ('Brainfuck') and you can definitely be too large ('C++'). There is a happy medium in there somewhere and the lack of a string type was perceived as a major shortcoming of C, but then again, if you realize that they didn't even have structs in the predecessor to C (even though plenty of languages at the time did have similar constructs) they got enough of it right that it ended up taking off.
C and personal computing hit their stride at roughly the same time, your choices were (if you didn't feel like spending a fortune) Assembly, C, Pascal and BASIC for most systems that mere mortals could afford. BASIC was terribly slow, Pascal and C a good match and assembler only for those with absolutely iron discipline. Which one of the two won out (C or Pascal) was a toss up, Pascal had it's own quirks and it was mostly a matter of which of the two won out in terms of critical mass. Some people still swear by Pascal (and usually that makes them Delphi programmers, which will be around until the end because the code for the heat-death of the universe was writting in it).
For me it was Mark Williams C that clinched it, excellent documentation, good UNIX (and later Posix) compatibility and whatever I wrote on the ST could usually be easily ported to the PC. And once that critical mass took over there was really no looking back, it was C or bust. But mistakes were made, and we're paying the price for that in many ways. Ironically, C enabled the internet to come into existence and the internet then exposed mercilessly all of the inherent flaws in C.
I suspect the problem is that every feature makes it possible for an entire class of algorithms to be implement much more efficiently and/or clearly with a small extension to the language.
Many people encounter these algorithms after many other people have written large libraries and codebases. It’s much easier to slightly extend the language than start over or (if possible) implement the algorithm in an ugly way that uses existing features. But enough extensions (and glue to handle when they overlap) and even a language which was initially designed to be simple, is no longer.
e.g., Go used to be much simpler. But in particular, lack of generics kept coming up as a pain point in many projects. Now Go has generics, but arguably isn’t simple anymore.
Haskell's user-facing language gets compiled down to Haskell "core" which is what the language actually can do. So any new language feature has a check in with sanity when that first transformation gets written.
George Orwell showed us that small languages constrain our thinking.
A small language but with the ability to extend it (like Lisp) is probably the sweet spot, but lol look at what you have actually achieved - your own dialect that you have to reinvent for each project - also which other people have had to reinvent time after time.
Let languages and thought be large, but only used what is needed.
I can take anything I wrote in C since ~1982 or so and throw it at a modern C compiler and it will probably work, I may have to set some flags but that's about it. I won't have to hunt up a compiler from that era, so the codebase remains unchanged, which increases the chances that I'm not going to introduce new bugs (though the old ones will likely remain).
If I try the same with a python project that I wrote less than five years ago I'm very, very lucky if I don't end up with a broken system by the time all of the conflicts are resolved. For a while we had Anaconda which solved all of the pain points but it too seems to suffer from dependency hell now.
George Orwell was a writer of English books, not a programmer and whatever he showed us he definitely did not show us that small programming languages constrain our thinking. That's just a very strange link to make, programming languages are not easily compared with the languages that humans use.
What you could say is that a programming languages' 'expressivity' is a major factor in how efficient it is in taking ideas and having them expressed in a particular language. If you take that to an extreme (APL) you end up with executable line-noise. If you take it to the other extreme you end up some of the worst of Java (widget factory factories). There are a lot of good choices to be found in the middle.
> What you could say is that a programming languages' 'expressivity' is a major factor in how efficient it is in taking ideas and having them expressed in a particular language
Rust does. You have editions to do breaking changes at the surface level. But that is per crate (library) and you can mix and match crates with different editions freely.
Thry do reserve the right to do breaking changes for security fixes, soundness fixes and inference changes (i.e. you may need to add an explicit type that was previously inferred but is now ambiguous). These are quite rare and usually quite small.
I'd normally agree that what you say is good enough in practice, but I question whether it meets GP's "absolute non-negotiable" standards. That specific wording is the reason I asked the question in the first place; it seemed to me that there was some standard that apparently wasn't being met and I was wondering where exactly the bar was.
Ada does. It has been through 5 editions so far and backwards compatibility is always maintained except for some small things that are documented and usually easy to update.
I'd normally be inclined to agree that minor things are probably good enough, but "absolute non-negotiable" is a rather strong wording and i think small things technically violate a facial reading, at least.
On the other hand, I did find what I think are the relevant docs [0] while looking more into things, so I got to learn something!
> except for some small things that are documented
I can't think of any established language that doesn't fit that exact criteria.
The last major language breakage I'm aware of was either the .Net 2 to 3 or Python 2 to 3 changes (not sure which came first). Otherwise, pretty much every language that makes a break will make it in a small fashion that's well documented.
Java rules here. You can take any Java 1.0 (1995) codebase and compile it as-is on a recent JDK. Moreover, you can also use any ancient compiled Java library and link it to modern Java app. Java source and bytecode backward compatibility is fantastic.
Java is very good here, but (and not totally it's fault) it did expose internal APIs to the userbase which have caused a decent amount of heartburn. If your old codebase has a route to `sun.misc.unsafe` then you'll have more of a headache making an upgrade.
Anyone that's been around for a while and dealt with the 8->9 transition has been bit here. 11->17 wasn't without a few hiccups. 17->21 and 21->25 have been uneventful.
Can confirm; my team spent the past 9 months upgrading an application JDK 8 -> 17, and there were breaking changes even after we got it compiling + running
Java has had some breaking changes (e.g., [0, 1]), though in practice I have to say my experience tends to agree and I've been fortunate enough to never run into issues.
It's probably borderline due to the opt-in mechanism, but Go did make a technically backwards-incompatible change to how its for loops work in 1.22 [0].
PHP has had breaking changes [1].
Ruby has had breaking changes [2] (at the very least under "Compatibility issues")
Not entirely sure whether this counts, but ECMAScript has had breaking changes [3].
The interesting thing about Go's loopvar change is that nobody was able to demonstrate any real-world code that it broke (*1), while several examples were found of real-world code (often tests) that it fixed (*2). Nevertheless, they gated it behind go.mod specifying a go version >= 1.22, which I personally think is overly conservative.
*1: A great many examples of synthetic code were contrived to argue against the change, but none of them ever corresponded to Go code anyone would actually write organically, and an extensive period of investigation turned up nothing
*2: As in, the original behavior of the code was actually incorrect, but this wasn't discovered until after the loopvar change caused e.g. some tests to fail, prompting manual review of the relevant code; as a tangent, this raises the question of how often tests just conform to the code rather than the other way around
You certainly won't find me arguing against that change, and the conservatism is why I called it borderline. The only reason I bring it up is because of the "absolute non-negotiable" bit, which I took to probably indicate a very exacting standard lest it include most widespread languages anyways.
Yes, I think it's also a good example of how "absolute" backwards compatibility is not necessarily a good thing. Not only was the old loopvar behavior probably the biggest noob trap in Go (*), it turned out not to be what anyone writing Go code in the wild actually wanted, even people experienced with the language. Everyone seems to have: a) assumed it always worked the way it does now, b) wrote code that wasn't sensitive to it in the first place, or c) worked around it but never benefitted from it.
*: strongest competitor for "biggest noob trap" IMO is using defer in a loop/thinking defer is block scoped
Strongly agree there. IMO breaking backwards compatibility is a tradeoff like any other, and the flexibility non-hardline stances give you is handy for real-world situations,
I'd normally agree with you in practice, but since "close enough" seems likely to cover most mainstream languages in use today I figured "absolute non-negotiable" probably was intended to mean a stricter standard.
C# for instance isn't such a "small language", it has grown, but code from older versions, that does not use the newer features will almost always compile and work as before.
The thing is that "most of them" seems incongruous with a demand for "absolute non-negotiable" backwards compatibility. If not for that particular wording I probably wouldn't have said anything.
I suspect this is fairly common. The reality is most developers don't need a lot of the features a language provides.
Python for example has a ton of stuff that can be done with classes using sunder methods and other magic. I'm aware of it, but in all the years I've been writing Python I've never actually needed it. The only time I've had to directly interact with it was when trying to figure out how the fuck OpenAPI generates FastAPI server code. Which fairly deep into a framework and code generation tool.
> What’s with all these new reference types?
> All of these are speculative ideas
makes it pretty clear to me that they are indeed not yet part of Rust but instead something people have been thinking about adding. The rest of the post discusses how these would work if they were implemented.
Own and uninit have been in discussions wrt in place construction. The Rust in the Linux kernel project seems to be the motivating use case for this that really got the effort going recently.
Yes that is annoying, but I don't know of any mainstream systems language that does. C and C++ can also have allocations anywhere, and C++ have exceptions. And those are really the only competitors to Rust for what I do (hard realtime embedded).
Zig might be an option in the future, and it does give more control over allocations. I don't know what the exception story is there, and it isn't memory safe and doesn't have RAII so I'm not that interested myself at this point.
I guess Ada could be an option too, but I don't know nearly enough about it to say much.
Zig doesn't have exceptions, it has error unions, so basically functions return either a value or an error code and the caller is forced by the language to note which was returned. And instead of RAII it has defer ... which of course can easily be forgotten or mis-scoped, so it's not safe.
>"Yes that is annoying, but I don't know of any mainstream systems language that does. C and C++ can also have allocations anywhere, and C++ have exceptions."
C++ has a way to tell to compiler that the function would raise no exceptions. Obviously it is not a guarantee that at runtime exception will not happen. In that case the program would just terminate. So it is up to a programmer to turn on some brain activity to decide should they mark function as one or not.
While this is 100% true for the system allocator, hitting OOM there you're likely hosed, it isn't true if you're using arenas. I work on games and being able to respond to OOM is important as in many places I'm allocating from arenas that it is very possible to exhaust under normal conditions.
i never got this point. whats stopping me from writing a function like this in zig?
fn very_bad_func() !i32 {
var GPA = std.heap.GeneralPurposeAllocator(.{}){};
var gpa = GPA.allocator();
var s = try gpa.alloc(i32, 1000);
s[0] = 7;
return s[0];
}
the only thing explicit about zig approach is having ready-to-use allocator definitons in the std library. if you excluded std library and write your own allocators, you could have an even better api in rust compared to zig thanks to actual shared behaviour features (traits).
explicit allocation is a library feature, not a language feature.
the explicit part is that zig forces you to import allocator of your choosing whereas odin has allocator passed as part of hidden context and you can change/access it only if you want to. hence explicit behavior vs implicit behavior.
i use neither of those languages, so don't ask me for technical details :D
This is something I do wish Rust could better support. A `#![no_std]` library crate can at least discourage allocation (although it can always `extern crate alloc;` in lib.rs or invoke malloc via FFI...)
Is the juice worth the squeeze to introduce two new function colors? What would you do if you needed to call `unreachable!()`?
It's a shame that you can't quite do this with a lint, because they can't recurse to check the definitions of functions you call. That would seem to me to be ideal, maintain it as an application-level discipline so as not to complicate the base language, but automate it.
Which are of course the wrong habits to form! (More seriously: in the contexts where such no-panic colors become useful, it's because you need to not call `unreachable!()`.)
> It's a shame that you can't quite do this with a lint, because they can't recurse to check the definitions of functions you call. That would seem to me to be ideal, maintain it as an application-level discipline so as not to complicate the base language, but automate it.
Indeed. You can mark a crate e.g. #![deny(clippy::panic)] and isolate that way, but it's not quite the rock solid guarantees Rust typically spoils us with.
> Typically no... which is another way of saying occasionally yes.
You might be able to avoid generating panic handling landing pads if you know that a function does not call panic (transitively). Inlining and LTO often help, but there is no guarantee that it will be possible to elide, it depends on the whims of the optimiser.
Knowing that panicking doesn't happen can also enable other optimisations that wouldn't have been correct if a panic were to happen.
All of that is usually very minor, but in a hot loop it could matter, and it will help with code size and density.
(Note that this is assuming SysV ABI as used by everyone except Windows, I have no clue how SEH exceptions on Windows work.)
> Indeed. You can mark a crate e.g. #![deny(clippy::panic)] and isolate that way, but it's not quite the rock solid guarantees Rust typically spoils us with.
Also, there are many things in Rust which can panic apart from actual calls to panic or unwrap: indexing out of bounds, integer overflow (in debug), various std functions if misused, ...
For your problem, i.e. "guessing whether a function will mutate the parent structure", the solution used by Rust is far from optimal and I actually consider it as one of the ugliest parts of Rust.
The correct solution for the semantics of function parameters is the one described in the "DoD requirements for high order computer programming languages: IRONMAN" (January 1977, revised in July 1977), which have been implemented in the language Ada and in a few other languages inspired by Ada.
According to" IRONMAN", the formal parameters of a function must be designated as belonging to one of 3 classes, input parameters, output parameters and input-output parameters.
This completely defines the behavior of the parameters, without constraining in any way the implementation, i.e. any kind of parameters may be passed by value or by reference, whichever the compiler chooses for each individual case. (An input-output parameter where the compiler chooses to pass it by value will be copied twice, which can still be better than passing it by reference, e.g. when the parameter is passed in a register.)
When a programming language of the 21st century still requires for the programmer to specify whether it is passed by value or by reference, that is a serious defect for the language, because in general the programmer does not have the information needed to make a correct choice and this is an implementation detail with which the programmer should not be burdened.
The fact that C++ lacks this tripartite classification of the formal parameters of a function has lead to the ugliest complications of C++, which have been invented as bad workarounds for this defect, i.e. the fact that constructors are not normal functions, the fact that there exist several kinds of superfluous constructors which would not have been needed otherwise (e.g. the copy constuctor), the fact that C++ 2011 had to add some features like the "move" semantics to fix performance problems of the original C++. (The problems of C++ are avoided when you are able to differentiate "out" parameters from "inout" parameters, because in the former case the function parameter uses a "raw" area of memory with initially invalid content, where an object will be "constructed" inside the function, while in the latter case the function receives an area of memory that has already been "constructed", i.e. which holds a valid object. In C++ only "constructors" can have a raw memory area as parameter, and not the normal functions.)
Another one that is missing in the article is &raw mut/const but it is purely for unsafe usage when you need a pointer to an unaligned field of a struct.
&raw T/&raw mut T aren't pointer types, they're syntax for creating *const T/*mut T.
These aren't included in the article because they are not borrow checked, but you're right that if someone was trying to cover 100% of pointer types in Rust, raw pointers would be missing.
I'm hoping that languages move away from the sigil-ified legacy of C treating pointers as special syntax and just start calling these `Ref<T>`, `PtrConst<T>`, etc.
Many "other languages", particularly ones that compile to native code in traditional way have fairly explicit ways of specifying how said parameters to be treated
> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!
Python at least is very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats ... are all passed by object reference. (Of course it's not relevant for tuples and scalars, which are immutable.)
Right, but I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages) I was asking about the foo.
Right, but that reference is all the function has. It can’t destroy another scope’s reference to the foo, and the Python GC won’t destroy the foo as long as a reference to it exists.
The function could mutate foo to be empty, if foo is mutable, but it can’t make it not exist.
>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!
No mention of references!
I don't care about references to foo. I don't care about facades to foo. I don't care about decorators of foo. I don't care about memory segments of foo.
"Did someone eat my lunch in the work fridge?"
"Well at least you wrote your name in permanent marker on your lunchbox, so that should help narrow it down"
then frobnicate may call foo.close(), or it may read foo’s contents so that you’d have to seek back to the beginning before you could read them a second time. There’s literally nothing you can do in frobnicate that can make it such that the 3rd raises a NameError because foo no longer exists.
>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!
#!/usr/bin/env python3
import inspect
def frobnicate(unfrobbed: any) -> None:
frame = inspect.currentframe().f_back
for name in [name for name, value in frame.f_locals.items() if value is unfrobbed]:
del frame.f_locals[name]
for name in [name for name, value in frame.f_globals.items() if value is unfrobbed]:
del frame.f_globals[name]
foo = open("bar.txt")
answer = frobnicate(foo)
print(foo)
Traceback (most recent call last):
File "hackers.py", line 20, in <module>
print(foo)
^^^
NameError: name 'foo' is not defined
Be careful with the absolutes now :)
Not that this is is reasonable code to encounter in the wild, but you certainly can do this. You could even make it work properly when called from inside functions that use `fastlocals` if you're willing to commit even more reprehensible crimes and rewrite the `f_code` object.
Anyway, it's not really accurate to say that Python passes by reference, because Python has no concept of references. It passes by assignment. This is perfectly analogous to passing by pointer in C, which also can be used to implement reference semantics, but it ISN'T reference semantics. The difference comes in assignment, like in the following C++ program:
#include <print>
struct Object
{
char member{'a'};
};
void assign_pointer(Object *ptr)
{
Object replacement{'b'};
ptr = &replacement;
}
void assign_reference(Object &ref)
{
Object replacement{'b'};
ref = replacement;
}
int main()
{
Object obj{};
std::println("Original value: {}", obj.member);
assign_pointer(&obj);
std::println("After assign_pointer: {}", obj.member);
assign_reference(obj);
std::println("After assign_reference: {}", obj.member);
return 0;
}
$ ./a.out
Original value: a
After assign_pointer: a
After assign_reference: b
Just like in Python, you can modify the underlying object in the pointer example by dereferencing it, but if you just assign the name to a new value, that doesn't rebind the original object. So it isn't an actual reference, it's a name that's assigned to the same thing.
ANYWAY, irrelevant nitpicking aside, I do think Python has a problem here, but its reference semantics are kind of a red herring. Python's concept of `const` is simply far too coarse. Constness is applied and enforced at the class level, not the object, function, or function call level. This, in combination with the pass-by-assignment semantics does indeed mean that functions can freely modify their arguments the vast majority of the time, with no real contract for making sure they don't do that.
In practice, I think this is handled well enough at a culture level that it's not the worst thing in the world, and I understand Python's general reluctance to introduce new technical concepts when it doesn't strictly have to, but it's definitely a bit of a footgun. Can be hard to wrap your head around too.
foo is a name. It's not at all clear what you mean by "the foo" ... the called function can modify the object referenced by the symbol foo unless it's immutable. If this is your complaint, then solve it with documentation ... I never write a function, in any language, that modifies anything--via parameter or in an outer scope--without documenting it as doing so.
> I don't care about the reference to foo (that's a low-level detail that should be confined to systems languages, not application languages)
This is not true at all. There's a big difference, for instance, between assigning a reference and assigning an object ... the former results in two names referring to the same object, whereas in the latter case they refer to copies. I had a recent case where a bug was introduced when converting Perl to Python because Perl arrays have value semantics whereas Python lists have reference semantics.
There seem to be disagreements here due entirely to erroneous or poor use of terminology, which is frustrating ... I won't participate further.
>> I sorely miss it in Python, JS and other languages. They keep me guessing whether a function will mutate the parent structure, or a local copy in those languages!
> Python at least is very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats ... are all passed by object reference. (Of course it's not relevant for tuples and scalars, which are immutable.)
Then let me just FTFY based on what you've said later:
Python will not be very clear about this ... everything, lists, class instances, dicts, tuples, strings, ints, floats, they all require the programmer to read and write documentation every time.
As they should, of course. And this is no different from the annotations that started this conversation, other than that one is enforced by the compiler ... but you never mentioned that.
reply