Overall I think Rust is a hands-down win over C and C++. People who want it to be like Go are probably not doing systems-level programming, which is what Rust is for, and I have severe doubts about whether a rich systems-level language could be made much simpler than Rust and still deliver what Rust delivers. If you want full control, manual memory management with safety, other safety guarantees, a rich type system, high performance, and the ability to target small embedded use cases, there is a certain floor of essential complexity that is just there and can't really be worked around. Your type system is going to be chonky because that's the only way to get the compiler to do a bunch of stuff at compile time that would otherwise have to be done at runtime with a fat runtime VM like Go, Java, C#.NET, etc. have.
Go requires a fat runtime and has a lot of limitations that really hurt when writing certain kinds of things like high performance codecs, etc. It's outstanding for CRUD, web apps, and normal apps, and I really wish it had a great GUI story since Go would be a fantastic language to write normal level desktop and mobile UI apps.
However, Rust's borrow checker is a very neat idea and one that is worthy of copying for new languages; or even some existing ones. Are there any other languages that have this at this point?
I think the issue with Rust is simply that it emerged out of the C/C++ world and they started by staying close to its syntax and concepts (pointers and references) and it kind of went down hill from there. Adding macros to the mix allowed developers to fix a lot of issues; but at the price of having code that is not very obvious about its semantics to a reader. It works and it's probably pretty in the eyes of some. But too me it looks like Perl and C had a baby. Depending on your background, that might be the best thing ever of course.
So I doubt rust will ever displace python and javascript for everyday app development. But it’s an excellent language for operating systems, game engines, databases, web browsers, IDEs and things like that where stability and performance matter more than cramming in ever more features.
It is surprising how far you can get with that approach without ever writing a sigle lifetime annotation.
Maybe what Rust needs is a “build a program with lots of allocations” and then “whittle down allocations with smart use of references” section of the book.
Rem acu tetigisti. This is what needs conveying. I think with Rust people are often speaking past one another. One person says "hey, you can't write EVERYTHING with meticulously-planned-out lifetimes, cows for every string, etc", and what they have in mind is a junior/novice programmer's first-time experience (or any combination thereof).
Whereas another person says "you can't clone everything everywhere, or wrap everything in a RefCell", and they are right about what they mean: that properly written software eventually, to be respectably efficient, needs to replace that kind of code with code that thoughtfully uses appropriate lifetimes to truly benefit from what Rust provides.
As usual, Wittgenstein is right, specifically in what he says in s.4 of his famous Buzzfeed article[0]: most so-called problems are merely confusions of language, where two people mistake an inconsistency in their use of signs for an inconsistency in their opinions. If they set it all out more fully and explicitly, I don't think there would be much disagreement at all about what's appropriate, at least among the 80% of reasonable people.
[0] https://www.buzzfeed.com/ludwigwittgenstein/fantastic-ways-t...
i.e. Java doesn't have value types and that makes the syntax a lot cleaner vs. C++, and I'm pretty sure Kotlin must be the same
So you could still have ownership in a language with references only, and the syntax could be cleaner than Rust.
In fact I think there was a post about just that: Notes on a Smaller Rust https://without.boats/blog/notes-on-a-smaller-rust/
That document is a few years old now and was intended as a design document. But Value classes shipped with Kotlin 1.5. Apparently they are compatible with the project Valhalla value objects that will be added to the JVM at some point. So, this stuff is coming.
I had to look it up because even though I write Kotlin a lot, value classes are not something I have used at all. Looks useful but not that big of a deal and doesn't really solve a problem I have. Data classes and records (in Java) are a bigger deal IMHO.
In practice, the way you deal with immutability in Kotlin is to keep most of your data structures immutable by default unless they need to be mutable. E.g. there's a List and a MutableList interface. Most lists are immutable unless you create a MutableList. Same with val vs. var variables. Val variables can't be reassigned and you kind of use var only by exception when you really have to. The compiler will actually warn you if you do it without good reason. A data class with only vals can't be modified. Java is a bit more sloppy when it comes to mutability semantics. It has records now but all the fields have setters by default. It has var but no val assignments (you can use final to force this but few people do). And so on.
Semantically this is not as strong as what Rust does of course but it's good enough to make e.g. concurrency a lot easier. Mostly, if you avoid having a lot of mutable shared state, that becomes a lot easier.
You could imagine a Kotlin like language with much stronger semantics implementing borrow checking instead of garbage collection. It wouldn't be the same language of course but I don't think it needs to be very different. Using it would not be a massively different.
That is the thing with guest languages, they cannot invent something that the plaform doesn't support, and if they indeed come up with lots of boilerplate code to fake features, they risk to become incompatible when the platform actually does provide similar features with incompatible semantics.
Together with Actors types/Distributed runtimes, Optional Automatic Reference Counting and good support on Linux, Swift is developing into a killer language imo.
[1] https://forums.swift.org/t/a-roadmap-for-improving-swift-per...
fn apply<A, B, C, G>(mut f: impl FnMut(B) -> G, a: A) -> impl FnMut(&B) -> C
// must still be `for<'r> impl FnMut(&'r B) -> C`, because that’s what filter requires
where
G: FnMut(A) -> C,
B: Copy, // for dereferencing
A: Clone,
{
move |b| f(*b)(a.clone()) // this must do any bridging necessary to satisfy the requirements
}
I wrote this code myself, and it's SLOW for me to read. Each part isn't hard, it's just too much crapIdeally, the language should have enough syntax-bending facilities so that you can still simulate what you want, this is mostly just operator overloading and not treating custom types like second class citizens. For example, your example of byte ptr ptr can be easily done in C++ by a bytePtrPtr struct, or even better, a Ptr<Ptr<Byte>> instantiated class from the template Ptr<T> for any T. Overloading the dereference and conversion operators will completely hide any trace of the fact it's not a built in type, and compiler optimization and inlinning will (hopefully, fingers crossed) ensure that no extra overhead is being introduced by the abstraction.
As for the 'byte ptr ptr' syntax specifically, in F# generic instantiation can be done by whitespace concatenation of type names in reversed C++/Java/C# order, so the above C++ type would (if translated to F# somehow) literally be written out as you want it to be, so even what seems like it would require language support (whitespace between related identifiers, generally a tricky thing in PL design) can actually be accomplished with clever and free minded syntax.
I wouldn't even say that. Using words (or multi-letter symbols generally) may be more to type, but virtually everybody uses editors with completion features and those completion features tend to work better with words than symbols. Furthermore, despite there being more characters, I don't think it's actually more to read. People who are fluent in reading languages written with alphabet systems don't read letter-by-letter, but instead read word-by-word, using effortless word recognition.
It all adds up. Languages like COBOL, PASCAL or ADA (originally designed for terminals with very limited character sets, sometimes even lacking lowercase text - thus requiring case-insensitive syntax) make it a lot harder to survey larger code blocks.
If that's true, I would expect logographic writing systems to cause less reader fatigue than alphabetic writing system. But as far as I'm aware that isn't the case, and the two are roughly equivalent.
Reminds me of this section of Rich Hickey talk: https://www.youtube.com/watch?v=aSEQfqNYNAc
I only occasionally dabble in Rust in my free time and coming back to a project of mine after months of not having used any Rust, yeah lets just say that line noise made me prematurely murder some of my pet-projects.
Sure it gets probably better with time but still it is a cost that one pays.
"I don't even see the code anymore. I just see blonde, brunette, ..."
I myself have just started to get like that with my understanding of Rust:
"I don't even see the `impl<P: 'static, const PS: usize> IndexMut<usize> for SomeThing<P, PS>` anymore. I just see a mutable iterator."
It is like that, because of all the chaining that one can do. It is also just a feeling.
Sometimes it's also the formatter though that outputs intensely sideways drifting code: https://github.com/rust-lang/rust/blob/1.60.0/compiler/rustc...
Let me put it another way: what Rust feature would you remove to "simplify" the language?
I suppose every thing in Rust has a raison d'etre but you pay with complexity that versatility. I think there is space now for a modern, memory-safe, SIMPLE, systems programming language. C has the backwards compatibility problem (although I am still bullish on its future) and a language like Zig never got any traction. Hopefully the future will bring new, interesting stuff.
As far as I can tell, Go was kinda rushed, and they called it 1.0 before they get to add the generics it obviously needed. And I bet adding those generics in a way that preserves backward compatibility was a bear.
Zig will be done when it's done.
To counteract it, I write exit-early code like this:
let foo_result = foo();
if let Err(e) = foo_result {
return Bar::Fail(e);
}
let foo_result = foo_result.unwrap();
... let foo_result = foo()
.map_err(Bar::Fail)?;You can write it like this:
let foo_result = match foo() {
Ok(v) => v,
Err(e) => return Bar::Fail(e)
}; let foo = match foo() {
Ok(foo) => foo,
Err(err) => return Err(err),
};
I was typing it out so often I made editor aliases `lmo` and `lmr` for Option and ResultThe code above can be written as:
let foo = foo()?; let Some(x) = foo() else { return 42 }; if let Ok(x) = my_func() {
// ...
}
Or do you mean something else?Of course, that's what goto is actually good for.
I feel this way about all the verbosity in rust - some of it could likely be inferred, but but having it all written down right where it is relevant is great for readability.
The functional syntax the author of this (good) article complains about is what this (long experience in procedural C like languages) old programmer has come to love.
This is going to sound absurd, but the only other language I had this experience with was Objective-C.
Verbosity is super underrated in programming. When I need to come back to something long after the fact, yes, please give me every bit of information necessary to understand it.
Objective-C makes everything verbose. It’s too far in the other direction. Memories of stringByAppendingString
F# can infer almost everything. It's easier to read when you do document some of the types though.
F# is also easier to avoid breaking in materially useful ways if (like TypeScript) you annotate return types even if they can be inferred. You'll get a more useful error message saying "hey stupid, you broke this here" instead of a type error on consumption.
I've made a couple small languages, and it's easy to end up lost in a sea of design decisions. But there are a lot of languages that have come before yours, and you can look to them for guidance. Do you want something like automatic semicolon insertion? Well, you can compare how JavaScript, Python[1], Haskell, and Go handle it. You can even dig up messages on mailing lists where developers talk about how the feature has unexpected drawbacks or nice advantages, or see blog posts about how it's resulted in unexpected behavior from a user standpoint.
You can also take a look at some examples of languages which are easy or hard to parse, even though they have similar levels of expressivity. C++ is hard to parse... why?
You'd also have as your guiding star some goal like, "I want to create an LL(1) recursive descent parser for this language."
There's still a ton of room for creativity within constraints like these.
[1]: Python doesn't have automatic semicolon insertion, but it does have a semicolon statement separator, and it does not require you to use a semicolon at the end of statements.
You can't look at JavaScript/Python/Go (I don't know about Haskell), because Rust is a mostly-expression language (therefore, semicolons have meaning), while JavaScript/Python/Go aren't.
The conventional example is conditional assignment to variable, which in Rust can be performed via if/else, which in JS/Python/Go can't (and require alternative syntax).
I have a hard time accepting this, because I have done exactly this, in practice, with languages that I've designed. Are you claiming that it's impossible, infeasible, or somehow impractical to learn lessons from -- uhh -- imperative languages where most (but not all) programmers tend to write a balance of statements and expressions that leans more towards statements, and apply those lessons to imperative languages where most (but not all) programmers tend to write with a balance that tips more in the other direction?
Or are you saying something else?
The fact that automatic semicolon insertion has appeared in languages which are just so incredibly different to each other suggests, to me, that there may be something you can learn from these design choices that you can apply as a language designer, even when you are designing languages which are not similar to the ones listed.
This matches my experience designing languages.
To be clear, I'm not making any statement about semicolons in Rust. If you are arguing some point about semicolon insertion in Rust, then it's just not germane.
I don't know which your languages are.
Some constructs are incompatible with optional semicolons, as semicolons change the expression semantics (I've given an example); comparison with languages that don't support such constructs is an apple-to-oranges comparison.
An apple-to-apple comparison is probably with Ruby, which does have optional semicolons and is also expression oriented at the same time. In the if/else specific case, it solves the problem by introducing inconsistency, in the empty statement, making it semantically ambiguous.
Just to give some more detail--you can find all sorts of reports from people who have implemented IDE support, talking about the issues that they've faced and what makes a language difficult to analyze syntactically or semantically. Because these discussions are available to sift through in mailing lists, or there are even talks on YouTube about this stuff, you have an wealth of information at your fingertips on how to design languages that make IDE support easier. Like, why is it that it's so hard to make good tools for C++ or Python, but comparatively easier to make tools for Java or C#? It's an answerable question.
These days, making an LSP server for your pet language is within reach.
Here's the example from the post:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
How would you prefer to write this?First, lifetimes are elided in most cases.
Second, the curly braces for the closure are not needed and rustfmt gets rid of them.
Finally, the "map" on result can be replaced with a return statement below.
So, in the end we get something like:
Trying::to_read(syntax, |like| this.can_be(maddening))?;
Ok(()) Trying\to_read\[&'a heavy](syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
I can't improve it that muche.g.
https://turreta.com/2019/12/24/pattern-matching-declarative-...
No memory management in Nim equals no memory safety guarantees. Or no? Well in that case the statement above is true.
> Nim, which technically accomplishes all (I assume) of the Rusty things that require syntax, manages to do it with quite a lot nicer syntax.
Nim does not have something which gives both memory safety and no ((tracing garbage collector) and/or (reference counting)) at the same time. End of story.
The fact that Nim has an off-switch for its automatic memory management is totally uninteresting. It hardly takes any language design chops to design a safety-off button compared to the hoops that Rust has to jump through in order to keep its lifetimes in check.
You are simply incorrect, appear unwilling to research why/appear absolutist rather than curious, and have made clear that what I think is "clarification" or "detail expansion" you deem "tedious" or "nitpicking" while simultaneously/sarcastically implicitly demanding more details. That leaves little more for me to say.
Nim is choice. :-) {EDIT: As DeathArrow also indicated! }
"Automatic vs manual" memory management is what a casual PL user probably cares about. So, "AMM" with later clarification as to automation options/properties is, I think, the best way to express the relevant ideas. This is why I said "tracing GC" and also why Nim has recently renamed its --gc:xxx CLI flags to be --mm:xxx.
Whether a tracing collector is even a separate thread or directly inline in the allocation code pathway is another important distinction. To muddy the waters further, many programmers often mean the GC thread(s) when they say "the GC".
What runtimes are available is also not always a "fixed language property". E.g., C can have a tracing GC via https://en.wikipedia.org/wiki/Boehm_garbage_collector and you can get that simply by changing your link line (after installing a lib, if needed).
People don't call reference counted C++ smart pointers "garbage collection", because they aren't managed by the runtime, nor optimized by the compiler, rather rely on basic C++ features.
But they call C++/CX and C++/CLI ref types, automatic memory management, exactly because they are managed by the UWP and CLR runtimes respectively,
https://docs.microsoft.com/en-us/cpp/cppcx/ref-classes-and-s...
https://docs.microsoft.com/en-us/cpp/dotnet/how-to-define-an...
I may be misreading your post as declaration rather than explanation of confusion, but on the one hand you seem to write as if "people not calling RC smart ptrs 'GC' is 'reasonable'" yet on the other both your two books include it as a form of "direct GC" - GC Handbook: The Art of AMM with a whole Chapter 5 and the other early in the abstract. darthrupert just reinforced "working programmer usage" being "not academic use" elsewhere. [2] GCHB even has a glossary - rare in CS books (maybe not in "handbooks"?) So, is your point "Academics say one thing, but 'People' another?"
C++ features you mention were intended to blur distinctions between "compiler/run-time supported features", "libraries", and "user code". Many PLs have such blurring. Such features, basic or not, are optimized by compilers. So, neither compiler support nor "The Runtime" are semantic razors the way I think you would like them to be (but might "explain people/working programmers"). If one "The" or "collection" vs. "collector" are doing a lot of semantic work, you are in confusing territory. Also, human language/terms are cooperative, not defined by MS. MS is just one more maybe confusing user here.
Between intentional blurriness, loose usage, and many choices of both algos & terms used in books, papers, documentation and discussions, and the tendency for people to just "assume context" and rush to judgements, I, for one, don't see existence of confusion as mysterious.
Given the confusion, there seems little choice other than to start with a Big Tent term like "memory management" and then qualify/clarify, though many find "not oversimplifying" tedious. I didn't think this recommendation should be contentious, but oh well.
https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...
Nim's modern memory management (ARC/ORC) is fairly similar to Rust. ARC functions by reference-counting at compile time and automatically injecting destructors: which is broadly comparable to Rust's ownership + borrow checker.
(A big difference is that Nim's types are Copy by default: this leads to simpler code at the expense of performance. You have control over this, keeping memory safety, with `var`, `sink`, and others, as highlighted in the above link.)
https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...
For reference cycles (the big limitation of reference counting), there's ORC: ARC + a lightweight tracing garbage collector.
As I understand it Rust also cannot handle reference cycles without manually implementing something similar.
https://nim-lang.org/blog/2020/12/08/introducing-orc.html
https://doc.rust-lang.org/book/ch15-06-reference-cycles.html
But when you do want to pass by reference: that's where Nim's move semantics come in. These are what are fairly similar to Rust's lifetimes and borrowing, and what the paste.sr.ht link briefly goes over.
If you're interested, you can read more about Nim's move semantics here:
I used to use Go, not much of a fan anymore, but I'm liking Crystal a lot to fill this space. Eventually Zig when it's more mature.
It feels dry, like a humourless android. Not very fun to write but probably the most pragmatic choice for the problem. I prefer having fun when I program.
Software is becoming more and more complex and unless there are entirely different design patterns we have failed to find, managing and understanding that during both the writing and the maintenance of software is the fundamental problem of our time. Someone else in these comments mentioned leaning more heavily into IDE tooling and I do wonder if we are coming to a point where that makes sense.
It’s not that we’ve failed to find different design patterns, it’s that we found these patterns in the 70s and haven’t done much with them since. Since C there has been a pretty constant march toward more imperative programming, but imperative programming I feel has reached its peak for the reasons you describe.
We’re only just starting to explore the functional programming space and incorporate those learnings into our work. But what about logic programming, dataflow programming, reactive programming, and other paradigms that have been discovered but not really fully explored to the extent imperative programming has been? I think there’s a lot of room for improvement just by revisiting what we’ve already known for 50 years.
That said I agree that we've barely scratched the surface of the space of good paradigms; I'm partial to logic programming but most are underexplored. Perhaps other approaches can use Rust or its IR (MIR?) as a compilation target. As an example, this strategy is being used by DDlog ( https://github.com/vmware/differential-datalog ).
I don't think we should dispense with it for that reason, but we also then have to admit imperative programming doesn't match the design of promised future hardware as well as it has past hardware. The future will be focused around manycore distributed heterogenous compute resources like GPGPU, neural cores, computer vision accelerators, cloud compute resources, etc.
Even languages with 'exotic' execution semantics like prolog's Unification is actually implemented with an imperative interpeter like the Warren Abstract Machine/WAM, and there's not an obvious path to implementing such unorthodox semantics directly in hardware. Low-level imperative programs aren't going away, they're just being isolated into small kernels and we're building higher level abstractions on top of them.
I am not convinced that there's so more to Rust than there is to GHC Haskell to justify so much dense syntax.
There's many syntax choices made in Rust based, I assume, on its aim to appeal to C/C++ developers that add a lot of syntactic noise - parentheses and angle brackets for function and type application, double colons for namespace separation, curly braces for block delineation, etc. There are more syntax choices made to avoid being too strange, like the tons of syntax added to avoid higher kinded types in general and monads in particular (Result<> and ()?, async, "builder" APIs, etc).
Rewriting the example with more haskell-like syntax:
Trying::to_read::<&'a heavy>(syntax, |like| { this. can_be( maddening ) }).map(|_| ())?;
Trying.to_read @('a heavy) syntax (\like -> can_be this maddening) >> pure ()
It's a tortuous example in either language, but it still serves to show how Rust has made explicit choices that lead to denser syntax.Making a more Haskell-like syntax perhaps would have hampered adoption of Rust by the C/C++ crowd, though, so maybe not much could have been done about it without costing Rust a lot of adoption by people used to throwing symbols throughout their code.
(And I find it a funny place to be saying _Haskell_ is less dense than another language given how Haskell rapidly turns into operator soup, particularly when using optics).
In fact, this invokes an associated function, `to_read`, implemented for the `Trying` type. If `Trying` was an instance `Trying.to_read...` would be correct (though instances are typically snake_cased in Rust).
I'll rewrite the line, assuming `syntax` is the self parameter:
syntax
.to_read::<&'a heavy>(|like| this.can_be(maddening))
.map(|_| ())?;
In my opinion, this is honestly not bad.Rust is not "avoiding" HKT in any real sense. The feature is being worked on, but there are interactions with lifetime checking that might make, e.g. a monad abstraction less generally useful compared to Haskell.
I started learning Rust recently and when searching how to do some pretty basic stuff, the answers are like "well you used to do this but now you do this and soon you'll be able to do this but it's not stabilized"
I figure I'll just check back in 5 years, I don't have the time or patience to be on the bleeding edge when I'm trying to get things done.
The frontend frameworks used to have really short lifecycles "Oh,you're still using FooJS? That's so last year. Everyone's on Bar.js now- it's so much better"
One of these days. In the meantime I'm rather happy with Go because it's easy for a code monkey like myself to do the needful.
Although I may have PTSD from Rust, because lately I find myself preferring Qbasic in my spare time. ¯\_(ツ)_/¯
True, but Rust is being used for a lot more than just system programming, judging from all the "{ARBITRARY_PROGRAM} written in Rust" posts here on HN.
Have you checked out C++20 concepts? It supports aliases and doesn't require explicit trait instantiations, making it possible to right such generic code with much less boilerplate.
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int>* = nullptr>
void func(T fp) { ... }
C++20: void func(std::floating_point auto fp) { ... } fn func<T: FloatingPoint>(fp: T) { ... }
And the "auto" variant is similar to impl argument in Rust: fn func(fp: impl FloatingPoint) { ... } template<std::floating_point T>
void func(T t) { ... }
(also, it's not really equivalent - if I'm not mistaken with traits you can only use what the trait declares ; in C++ you can for instance do something like template<typename T>
concept CanBlah = requires (T t) {
t.blah();
};
and still be able to do void func(CanBlah auto t) {
log << "func:" << t;
t.blah();
}
instead of polluting the prototype with all possible side concerns)C++ Concepts are duck typed, and Rust's Traits are not, so in Rust you are expressing meaning here, and in C++ only making some claims about syntax which perhaps hint at meaning.
WG21 seems to dearly wish this wasn't so, offering Concepts which pretend to semantics they don't have, such as std::totally_ordered and I'm glad to see your "CanBlah" concept doesn't do this, to be sure all things which match this requirement can, indeed blah() although we've no idea what that can or should do.
Once you've accepted that you only have duck typing anyway, you're probably going to have to explain in your documentation the actual requirements for this parameter t, as the prototype merely says it CanBlah and that's not actually what we care about.
In contrast the Rust function we looked at actually does tell us what is required here, something which "implements FloatingPoint", and that implementation (plus the data structure itself) is all that's being exposed.
> WG21 seems to dearly wish this wasn't so
how so ?
> Once you've accepted that you only have duck typing anyway, you're probably going to have to explain in your documentation the actual requirements for this parameter t, as the prototype merely says it CanBlah and that's not actually what we care about.
the documentation having to state "t can be logged" would just be useless noise and a definite no-pass in code review aha
> In contrast the Rust function we looked at actually does tell us what is required here, something which "implements FloatingPoint", and that implementation (plus the data structure itself) is all that's being exposed.
my personal experience from other languages with similar subtyping implementation (ML-ish things) is that this looks good in theory but is just an improductive drag in practice
C++ didn't have any other practical choice here.
But this introduced another case where C++ has a "false positive for the question: is this a program?" as somebody (Chandler Carruth maybe?) has put it. If something satisfies a Concept, but does not model the Concept then the C++ program is not well formed and no diagnostic is required.
> how so ?
I provided an explanation with an example, and you elided both.
> the documentation having to state "t can be logged" would just be useless noise and a definite no-pass in code review aha
In which case it's your responsibility to ensure you can log this, which of course CanBlah didn't express.
> similar subtyping implementation
The only place Rust has subtyping is lifetimes, so that &'static Foo can substitute for any &'a Foo and I don't think that's what you're getting at.
I don't understand how random assumptions on what WG21 may or may not think counts as an example (or anything to be honest)
> If something satisfies a Concept, but does not model the Concept then the C++ program is not well formed and no diagnostic is required.
uh... no ?
I think that you are referring to this blog post: https://akrzemi1.wordpress.com/2020/10/26/semantic-requireme... for which I entirely disagree with the whole premise - the only, only thing that matters is what the compiler understands. The standard can reserve itself the right to make some cases UB, such as when trying to sort un-sortable things just like it can state that adding to numbers can cause UB and that's fine: it's the language's prerogative and is all to be treated as unfortunate special-cases ; for the 99.99999% remaining user code, only the code matters and it makes no sense to ascribe a deeper semantic meaning to what the code does.
> In which case it's your responsibility to ensure you can log this, which of course CanBlah didn't express.
A metric ton of side concerns should not be part of the spec, such as logging, exceptions, etc - everyone saw how terrible and counter-productive checked exceptions were in java for instance. Specifying logging explicitly here would be a -2 in code review as it's purely noise: the default assumption should be that everything can log.
> The only place Rust has subtyping is lifetimes, so that &'static Foo can substitute for any &'a Foo and I don't think that's what you're getting at.
I meant polymorphism, my bad
Actually, yes. I was hoping people like you were familiar, but that was actually more to ask of you than I'd assumed since C++ has a tremendous amount of such language in the standard, going in I'd figured hey maybe there's a half dozen of these and I was off by at least one order of magnitude. That's... unfortunate.
Exactly which language ended up being in the "official" ISO standard I don't know, but variations on this are in various circulating drafts through 2020 "[if] the concept is satisfied but not modeled, the program is ill-formed, no diagnostic required", if you're trying to find it in a draft you have, this is in the Libraries section in drafts I looked at, although exactly where varies. [ Yes that means in principle if you completely avoid the C++ standard library this doesn't apply to you... ]
> I think that you are referring to this blog post
It's possible that I've read Andrzej's post (I read a lot of things) but I was just reporting what the standard says and all Andrzej seems to be doing there is stating the obvious. Lots of people have come to the same conclusion because it isn't rocket science.
> only the code matters and it makes no sense to ascribe a deeper semantic meaning to what the code does.
This might be a reasonable stance if the code wasn't written by people. But it is, and so the code is (or should be) an attempt to express their intent which is in fact semantics and not syntax.
But let's come back to std::totally_ordered, although you insist it doesn't "count as an example" for some reason, it is in fact a great example. Here's a standard library concept, it's named totally_ordered, so we're asking for a totally ordered type right? Well, yes and no. Semantically this is indeed what you meant, but C++ doesn't provide the semantics, C++ just gives you the syntax check of std::equality_comparable, and if that's a problem you're referred to "No diagnostic required".
Couldn't find anything ressembling this in the section of the standard describing concepts and constraints. The spec is very clear (C++20 7.5.7.6):
> The substitution of template arguments into a requires-expression may result in the formation of invalid types or expressions in its requirements or the violation of the semantic constraints of those requirements. In such cases, the requires-expression evaluates to false; it does not cause the program to be ill-formed.
Maybe the stdlib has different ording, but the stdlib can literally have any wording it wants and could define std::integer to yield 2+2 = 5 without this being an issue.
> [ Yes that means in principle if you completely avoid the C++ standard library this doesn't apply to you... ]
in just a small library i'm writing, there's already ten-fold the number of concepts than there are defined in the standard library, so I'd say that this does not apply in general ; the stdlib is always an irrelevant special case and not representative of the general case of the language, no matter how hard some wish it. E.g. grepping for 'concept [identifier] =' in my ~ yields 2500 results, with only a small minority of those being the std:: ones.
> This might be a reasonable stance if the code wasn't written by people. But it is, and so the code is (or should be) an attempt to express their intent which is in fact semantics and not syntax.
I think this is very misguided. I am not programming for humans to process my code, but for computers to execute it. That's what comes first.
> Semantically this is indeed what you meant,
no, if I type std::totally_ordered, I mean "whatever the language is supposed to do for std::totally_ordered", and exactly nothing else
That's easy then, if you mean "whatever the language is supposed to do for std::totally_ordered" you could say what that is exactly, right?
They can be ergonomic high level, while providing the language features to go low level when needed.
Are you sure? What stops Swift with its beautiful syntax and safe optionals from becoming a systems language?
I do love Swift and would use it for systems stuff in a heartbeat if I could, but there are also some downsides that make it pretty awkward for systems. The performance isn't always the best but it's (generally) very clear and ergonomic.
The "cfg" directive is closer to the syntax used in ".toml" files than to Rust itself, because some of the same configuration info appears in both places. The author is doing something with non-portable cross platform code, and apparently needs more configuration dependencies than most.
But yes, there is still a lot of it.
Anyway, most of the noise comes from the fact that Rust is a low level language that cares about things like memory management. It's amazing how one is constantly reminded of this by the compiler, what is annoying, but the reason it doesn't happen on the alternatives is because they never let you forget about that fact.
For instance a function which returns an optional pointer to a 'Bla':
fn make_bla() ?*Bla {
// this would either return a valid *Bla, or null
}
A null pointer can't be used accidentally, it must be unwrapped first, and in Zig this is implemented as language syntax, for instance you can unwrap with an if: if (make_bla()) |bla| {
// bla is now the unwrapped valid pointer
} else {
// make_bla() returned null
}
...or with an orelse: const bla = make_bla() orelse { return error.InvalidBla };
...or if you know for sure that bla should be valid, and otherwise want a panic: const bla = make_bla().?;
...error handling with error unions has similar syntax sugar.It's probably not perfect, but I feel that for real-world code, working with optionals and errors in Zig leads to more readable code on average than Rust, while providing the same set of features.
The main difference I see is that in Rust it will also work with your own custom types, not just optional.
fn make_bla() -> Option<Bla> {
// this either returns a valid Bla, or None
}
if let Some(bla) = make_bla() {
// bla is now the unwrapped valid type
} else {
// make_bla() returned None
}
..or with the '?' operator (early return) let bla = make_bla().ok_or(InvalidBla)?;
..or with let_else (nightly only but should be stable Soon(tm)) let Some(bla) = make_bla() else { return Err(InvalidBla) }
..or panic on None let bla = make_bla().unwrap(); ??i32 const assert = @import("std").debug.assert;
fn get12() i32 {
const opt_opt_val: ??i32 = 12;
const val = opt_opt_val.?.?;
comptime assert(val == 12);
return val;
}
[https://www.godbolt.org/z/oncd5shvP]I'm not familiar enough with it to know how much is truely in the protocol vs what the editor still has to do themselves.
The problem is that most on FOSS community tend to design languages for VI/Emacs workflows.
```pub fn horror()->Result{Ok(Result(mut &self))}```
A function returns a Result. This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:
```pub fn better->self```
No matter what it takes to make the compiler smarter.
That is not, in fact, a core concept in Rust. Plenty of functions have no reason to return Result. (And some that do also have a reason for the inner class to be a result.)
> This concept in Rust is so ubiquitous that it should be a first class citizen. It should, under all circumstances, be syntactically implicit:
“Implicit” is an opposed concept to “first-class citizen”. Result is first-class in Rust, and would not be if function returns were implicitly Result.
If you don't see std::result::Result as a core concept in Rust, which might be fair, one can still argue that it _should_ be a core concept, given its ubiquitous usage.
What I said is that “A function returns Result” in the universal sense (that is, everything that is a function returns Result) is not a core concept in Rust.
Some functions return Result<T,E> for some <T,E>. Some functions return Option<T> for some T. Some functions have no reason to use that kind of generic wrapper type (a pure function that handles any value in its range and returns a valid value in a simple type for each doesn't need either; Option/Result are typically needed with otherwise non-total functions or functions that perform side effects that can fail.)
I wouldn't use the emotionally-loaded dismissive language, but, yes, Result is a solution to the same problem as exceptions that deals with several problems of exceptions, including:
(1) Unchecked exceptions obscure what is going on, and frustrate analysis because things remote from the code may bypass it in the call-stack without any evidence visible in signatures.
(2) Checked exceptions are clear, but create a separate syntax for expressing type-like constraints, also limiting what you can do around them because they aren't the same thing as types.
Results are basically cleaner checked exceptions.
I love Rust to death, but I feel this is a huge flaw in its design. Writing any unsafe code that takes a callback function is a massive pain in the ass, because anytime you call this callback, it could panic, so you must restore into a safe state if stack unwinding occurs.
For example, I'm currently writing a fast sorting algorithm, using mostly unsafe code (by necessity). Every time I compare two elements, a panic could occur, and all elements have to be placed back in the input array to restore to a safe state. It's hellish.
There is the panic = abort flag which is great if you're writing a binary. But as a library you can not assume this.
IMO, one of the most valuable pieces of exception handling is a distinct separation between your error logic and your non-error logic, which makes methods easier to comprehend. I also feel like the existence of the ? syntax is a dead giveaway in this regard because it's a fig-leaf trying to cover up the most egregious parts of the code where you'd otherwise have to be write the frequent "if error then early return error" statements which plague Golang.
It avoids creating a separate syntax from the usual return-type declaration syntax for declaring the existence of errors, when compared to checked exceptions.
It also avoids creating a separate syntax for error handling, compared to (checked or unchecked) exceptions (pattern matching is ubiquitous in Rust for other purposes).
> It's created a different, more complex syntax which must be adopted inline in your actual normal code path, obfuscating what your code is actually expected to do under non-error conditions.
Pattern matching isn't an additional syntax (indeed, many languages with exceptions also have it), and it (IMO) does less to obscure non-error code than the visual noise of handling errors with try/catch.
It is more visual noise in the case of functions that do the equivalent of not handling exceptions, compared to exception-using langauges where that is implicit.
Besides, what is the error type for Result? You haven't declared it.
For one thing, we already have syntactic collisions that don't seem to cause much problem (consider `foo?.bar` in .ts vs .rs), and this one would probably be prevalent enough that it would quickly be familiar to anyone using the language.
For another, if we squint I'm not sure those other languages aren't "really" using it for the same thing. If in some module we define `type Result<T> = Option<T>` then we have the same behavior in Rust, and we can imagine that those other languages have basically done so implicitly, meaning it's a less flexible version of the same mechanism (put to slightly different purposes).
Also, the ownership mode, a concept entirely missing from Kotlin or Scala.
As GP says, Rust's syntax is pretty noisy, but much of the noise is answers to questions other languages don't even ask.
And many complains are requests for additional noise for things which are just regular in Rust, like additional syntactic sugar for Option and Result.
I also personally find Go syntax to be horrible, especially now with generics.
I personally consider better use Go than C for such purposes, even if they aren't "systems programming".
https://news.ycombinator.com/item?id=31227986
This perpetual debate reminds me of the trouble HN used to have with the concepts of "contractors" and "consultants", where any time someone mentioned that they were doing consulting work there'd be an argument about whether it was in fact contracting. It's a message board tic, is what I'm saying, not a real semantic concern.
Pike: When we first announced Go we called it a systems programming language, and I slightly regret that because a lot of people assumed that meant it was an operating systems writing language. And what we should have called it was a 'server writing language', which is what we really thought of it as. As I said in the talk before and the questions, it's turned out to be more generally useful than that. But know what I understand is that what we have is a cloud infrastructure language because what we used to call servers are now called cloud infrastructure. And so another definition of systems programming is stuff that runs in the cloud.
Alexandrescu: I'm really glad I let Rob speak right now because my first question was 'go introduces itself as a systems programming language' and then that disappeared from the website. What's the deal with that? So he was way ahead of me by preempting that possible question.
So it seems to me that they struggle with the nuances of the concept as much as the commenters here, particularly as it pertains to Golang.
Despite my opinion on Go's design, I rather would like to see people using Go instead of C for such use cases.
Although I guess JIT compilers may be classified as systems programming, since you need to do some OS-specific tricks with virtual memory during execution of the compiler.
Languages that are not systems language trade-off this capability for other benefits like concise expressiveness and ease of use.
Of course, the garbage collector did not exactly make it easier - but it's an interesting piece of software.
but it's an interesting piece of software.
Agreed. And I didn't mean to imply that it's impossible to use Go that way, but I think it's fair to say that it's less common and perhaps even less desirable to do that.
OTOH, people have written (at least parts of) Operating Systems in Java[1] even, so never say never...
You can write a database with it but that makes it an application language, not system.
Otherwise you could call every language a "system language" and the distinction would lose all meaning.
https://golangdocs.com/system-programming-in-go-1
EDIT: formatting
I'm a huge Rust fan, but sort of agree. First, I dislike C-style syntax in general and find it all very noisy with lots of unnecessary symbols. Second, while I love traits, when you have a trait heavy type all those impl blocks start adding up giving you lots of boilerplate and often not much substance (esp. with all the where clauses on each block). Add in generics and it is often hard to see what is trying to be achieved.
That said, I've mostly reached the conclusion that much of this is unavoidable. Systems languages need to have lots of detail you just don't need in higher level languages like Haskell or Python, and trait impls on arbitrary types after the fact is very powerful and not something I would want to give up. I've even done some prototyping of what alternative syntaxes might look like and they aren't much improvement. There is just a lot of data that is needed by the compiler.
In summary, Rust syntax is noisy and excessive, but I'm not convinced much could have been done about it.