The struggle with Rust(ayende.com) |
The struggle with Rust(ayende.com) |
I believe that we are seeing the same kind of phenomenon in Rust: we are trying to replicate C or C++ code directly in Rust, and we get frustrated when our efforts are foiled by the borrow checker. It's going to take some time and some efforts before we learn and internalize how borrowing works in Rust and how to change the way we write programs so that it is no longer an obstacle. The Rust team looks very receptive to comments about taking away some pain points (e.g., non-lexical lifetimes), but we also need to accept that, for a little while, we are going to feel like we did when we were new programmers and were trying to make sense of these new constraints.
While Rust has mechanisms to deal with this (copying, Rc<T>, etc.), all these mechanisms add pain points, and for problems that are not just theoretical. It affects a number of design patterns, closures that survive the scope they were created in, functional-style programming, functional data structures, any data structure that naturally involves a DAG or cycles.
The difference compared to a (good) static type system is that a good [1] static type system does not measurably reduce expressiveness; Rust's ownership model does.
This is not to say that there isn't a point to Rust's model. Rust directly competes with garbage collection as the main alternative model to achieve memory safety and is an obvious choice in cases where garbage collection is not a realistic option. But where garbage collection is an option, the tradeoffs that Rust makes become much less attractive.
[1] Obviously, primitive static type systems (example: Java up to version 1.4) do pose a problem.
Second, this article only considers the costs of a stricter borrow checker, not the benefits. It will be more work for the coder to have to think about how allocation and ownership is communicates. But I've been through much bigger headaches in production code at inconvenient times (for the business plan) due to dangling pointers, multi-writer concurrency bugs, and ill-defined memory models in software designs.
Even if Rust is much harder than alternatives instead of just different, and even if that's inherent in borrow checking, it's not clear that it's still not worth it in the long run.
This is pseudo-jargon. What counts as a "measurable" reduction? Do you have any substantive evidence that "good" typesystems don't reduce it? And why are only type systems which satisfy this property "good"? You talk cogently about tradeoffs later on, but this sentence taken literally suggests only a type system which doesn't make this particular tradeoff can be "good". Why?
My go-to languages for the past few years have been Haskell and Racket. There have definitely been times I picked Racket because I knew Haskell's type system would interfere with my expressiveness. Certainly Haskell is less restrictive than Rust with regards to ownership, but all type systems restrict you. I think GP is spot-on in pointing out that you just get used to it over time as you learn to think and code in ways that avoid pushing against the type-system's weak spots.
> But where garbage collection is an option, the tradeoffs that Rust makes become much less attractive.
Yes, precisely. It's not a fundamentally different issue from getting used to other static type-systems; it's just a different tradeoff.
I also agree about your other points, I think maybe one of these days the borrower will become more sophisticated, but these issues he's complaining about are the exact reason I put Rust down. It just felt too sophomoric, as if the language wasn't really ready.
Not all is bad, I really like the way errors are handled in Rust, but there are definitely some problems with the approach they take.
I don't know. I picked up OCaml fairly quickly, but I have consistently struggled with Rust, trying it for a while before abandoning it in frustration with lifetimes, 2 or 3 times. I can program in Rust, but it still seems like an ongoing struggle, as opposed to the smooth brain to text programming I've gotten used to in Python, JavaScript, OCaml, and Lua, and to a lesser extend C, Go and Elixir (just because I haven't used to latter 3 as much, at least recently).
I think Rust may be ideal for embedded software development, or low-level systems software, but for general application development, I think OCaml or perhaps Swift or Scala are more ideal, at least for me (or at least until I decide to try Rust again, perhaps it will stick this time).
Edit: Oh yeah, and I was even able to become productive enough in Scala to build a significant project (a programming language) in a relatively short period of time. Now, I'm not silly enough to think I've really mastered Scala in such a short period of time, but I was using it productively as a functional language as I would OCaml with little difficulty. On the other hand, I've started 3 (3!) programming languages in Rust but have yet to get beyond much beyond the parser/AST stage before I quit in frustration.
This isn't a good thing, in my opinion. Brains make mistakes. I don't want smooth, uninterrupted coding. I want the compiler to tell me when I'm making the mistake, even if it makes things choppier. It's a lot less painful to write code in fits and starts than to write a million tests or spend hours debugging.
If you're not aware of it, scala-native looks very interesting... It looks to be active project.
https://github.com/scala-native/scala-native
FWIW, I've also enjoyed Swift development quite a bit. I think Swift will go deep into the C++ sweet spot.
Coming from C/C++, I know that there are structs in memory and can visualize what they look like - surely I can just go and tweak them? (No I can't because either it wasn't safe to do that anyway, or I need to explain to the Rust compiler why it is safe, and that can be quite difficult.)
Coming from something like Java, I (probably) don't think too much about what the underlying structure in memory is, so I'm less likely to try to do this.
Either way, it feels like part of the learning curve is learning the "Rustic" way to do something - in general, you probably shouldn't be reaching for "unsafe" until you know what you're doing! ;)
I looked at Rust specs 1.0 when they were released and immediately recognized that Rust is not a 'dating' language, but rather a very serious 'marriage' till death do us part sort of deal. (I would say the same for Haskell, & Scala.) And imho the initial promise of addressing the out of control complexity of C++ was not met.
I have no doubt that Rust is a thoughtful, and powerful, language. But you have to be prepared to exchange vows at the alter.
I'm not sure. Type traits seem to be much more manageable than how the same thing is handled in C++ template code, no?
Also, if we can bring in ecosystem effects such as cargo...
Personally, I used OCaml while at university to write a programming language (http://jsjs-lang.org) and totally loved it. Real world OCaml is a great resource and I'd definitely recommend giving OCaml a try. My only regret is that I don't get to write OCaml as much as I want to due to the kind of projects I work on, but given that Bucklescript[0] and Reason[1] are moving ahead in full steam, that might change shortly too :)
It's definitely worth playing around with a language from that family. OCaml is maybe slightly more practical than Haskell and it has some nice resources available - in particular Real World OCaml looks really good: https://realworldocaml.org/
If you already know Rust I wouldn't bother learning both - they're very similar languages.
1. Go unsafe on lowest-level operations that simply can't be GC'd. Quite a few of these can be expressed as finite, state machines which can be model-checked for correctness. Static analyzers are also really good these days in whatever code you can avoid dynamic allocation in. The rest of the code is GC'd. That's what the Oberon operating systems do.
2. Use a real-time, low-latency, concurrent GC that basically knocks low single or two digit performance out of the system. Lots of products in embedded Java have special GC's for that. Buy a faster CPU. Trade dollars or performance for productivity and safety.
3. Use a mix of GC's in the system where each one is tuned for the job at hand. You might use a real-time, concurrent one in the kernel. Maybe just reference counting in one more about performance. Maybe a straight-forward one for stuff that's not performance-critical. Could even use one that's mathematically verified for correctness in that situation. JX Operating System is an example of OS that mixes up GC strategies.
That's for software. Your options open up a bit if you can also deploy custom hardware. At the least, a modified soft-core on an FPGA. The LISP machines & maybe a Java CPU of the past often included hardware acceleration for garbage collection. One built the GC right into memory subsystem/interface of the processor where neither the processor nor applications had to be aware of its existence. It just did its thing in parallel to execution of the app. A concurrent, low-latency one integrated with virtual, memory hardware shouldn't take much hardware resources to pull off.
Anyway the syntax is so similar you can learn one and switch to the other with very little pain.
I remember asking on freenode's #ocaml for help with a convoluted error message, and when I was told what it meant and how to fix it, I asked how the person who helped me knew what it meant. He told me he'd taken two semesters of type theory courses at uni. Having to spend a year taking university-level courses in order to effectively program in OCaml seemed like way too much pain for too little gain when there were so many other less painful languages to program in.
Later I discovered Lisp and then Scheme. Both were a sheer joy to use compared to OCaml. I didn't have to wrestle with the type checker, my programs were quick and easy to write and understand, and in their own way the languages were just as powerful as OCaml was. Sure, OCaml had them beat at safety (at least if you were comparing ordinary Lisps and Schemes, not statically typed versions or extensions with more safety features) but it way, way more painful and slow to program in, for me. I recognize that if you need the safety, then the pain might be worth it. But even then it might be worthwhile to program in a dynamic language like Lisp/Scheme and then once the program matures, rewrite it in a safe language like OCaml. But even then, I think I'd just rather dial up the safety features of Lisp/Scheme or use one of the static versions.
That's just me. I know some people love OCaml. And I'm sure I'm missing some of its wonders that are only apparent when you dive deep enough in to the language. But from what I did learn of it, it just wasn't for me.
Unfortunately, things aren't that simple and Rust really is different from other languages. It takes months of steady investment (and yes, frustration), but the payoffs are spectacular. I would urge the author to persevere. In the meantime, the community is very helpful and I'm sure you'll get lots of useful advice on the back of this post.
I can't take this article seriously after reading that and after reading this:
So I spent a few evenings with Rust
How long did it take you to learn C, C++, Java, Python, Ruby, Perl or other languages? MONTHS. Many many many months. Sure you could get something up and running in a few evenings but after many months did you realize that earlier code could be better. Each language has its own obstacles and spending a few evenings isn't enough time to get around them.
I gave Haskell a pass but after checking out Elm I can see more of its power. I gave Rust a pass because the language was changing but now that it's stable I expect to sit down for a few months at least learning it.
* Read the book, got some of the concepts but most of it went over my head.
* Did nothing, maybe thought about it a little bit.
* Two weeks later: read the book again. It made more sense but some of it is still lost on me.
* Actually wrote some Rust. Was hard, frequently consulted the book, the "by example" book, r/Rust, and google.
* Two weeks later, it's much easier. I'm still consulting outside resources frequently, but I generally know how to fix problems as they arise and my mental model of the how the language executes is becoming more and more accurate.
I've spent on average 30 minutes a day working on a pretty simple Rust program for about a month now. The advantages of Rust so far have been: if it compiles, it works. Extremely easy to refactor: for my main data structure, I've switched between vecs and arrays back and forth probably 4 times to get a feel for the differences. This is generally really easy to do because they both implement the the iterator trait. In the other languages I use (at work), switching between one type of collection and another would be a huge time sink and involve a lot of annoying (though trivial) refactoring.
The main advantage of Rust for me (and the reason why I decided to check it out vs go) is that I don't have a lot of experience thinking about memory at a low level. The dynamic languages I work with abstract that away from the programmer. I wanted something that would force me to think about how and where memory is being allocated, and whether I want to pass by reference or value. For someone who has spent a lot of time with C/C++ or just someone with a background in CS/programming languages, this may all seem obvious to you. But there's a lot of programmers out there who learned with dynamic languages and don't have a good mental model for memory. Rust is a good instructional tool for us.
I don't really have an opinion on whether Rust is "production ready." It's being developed rapidly though, so I think it'll get there.
To be fair, if you want to call malloc it is still just as accessible[1][2] as it is in C. If you search the Rust documentation for malloc `libc::malloc` is even the first result. If you were to use that on stable Rust the error message will even tell you to link your program to the libc published on crates.io (which is just adding a line in your Cargo.toml file.)
I'm not sure I'd call that jumping through hoops. I'd love if Rust provided easy access to its own allocator on the stable release channel; but I'm willing to wait for them to get it right, these things take time.[3]
[1]: https://doc.rust-lang.org/libc/x86_64-pc-windows-msvc/libc/f...
[2]: https://is.gd/cgkX2R
[3]: https://github.com/pnkfelix/rfcs/blob/117e5fca0988a1b0c4b6e4...
RawVec is an implementation detail; Vec exposes most of what you need for dealing with raw memory.
This kind of attitude towards people trying out a new language is damaging to the person trying the new language and to people who are part of that languages community. The former because it will give them the notion that the community is hostile towards them and newcomers in general and the latter because it can enforce a dogmatic view of the language ("you do it our way or no way!" kind of feel).
A language's ability to have people pick it up easily is important. In Rust's case there is definitely always going to be a bit of a hump for people to get over, but the concern put forth in this article seems like a legitimate one, but one the Rust community seems to be aware of. Acknowledging and helping people with these problems (while searching for a way to potentially avoid such a situation for the next newcomer) is important and it's critical that you not simply dismiss someone's opinion because they don't know or understand "the right way to do it" yet.
I'm reluctant to even listen to someone's Amazon product opinion if they've only used it 2 days.
Get out a quick correct (and this can not be emphasized enough) version (obviously one has to learn the language here) and then optimize (when you have numbers). The author did not seem to get a working version with Rust (sorry, if I am wrong on this), and tried an approach that he knows would work for C/C++. But maybe another approach would have been already performant enough?
I suggest writing applications, or libraries closer to applications (e.g. a specific library that parses a special data format important to your domain), where you have a clear goal but not a clear path to the goal in mind. In other words, hacking.
Let's say the author had finished the Trie. What good experience could he possibly have? It's not going to magically run faster than the C++ version or use less memory. It's a well-known algorithm so there probably aren't subtle bugs in the C++ version that rust would have caught. The best you could possibly say is that it was faster to write -- which is exactly why it skews the experience toward familiar and permissive languages.
I am trying to learn rust (albeit very slowly) by doing some hacking to make it work better for postgres extensions. I know postgres, and I think it would be cool if it were easier to write postgres extensions in rust. Of course you can today, but you'd have to use a lot of unsafe FFI calls.
Use unsafe. No, really, just use unsafe.
Clearly a big value of Rust is that it's a safe language, and the design patterns around safety and affine typing are emerging and good. We should want people to write and use safe code as much as possible. But the trie example seems, to me, to be very much the sort of thing unsafe is for. Writing in C++ is essentially doing everything in unsafe. Unsafe is not evil; it's an important part of the language and when it's called for, it's called for.
If your goal is to use malloc and stuff (which seems to be what the author wanted) then of course you should use unsafe. Unsafe is indeed for doing abstraction-writing like this. Still, you should avoid it if you can.
It's fast, has a Python-like syntas, a good degree of memory safety and optional GC.
Not me, by the way, I don't know rust. But I reckon it'll be about three comments down.
I stopped reading. There is literally nothing you could possibly say that'd be worthwhile after "a few evenings" with Rust, especially when you lack the proper background in the first place.
This trend of "The problem with Rust: A naive and inexperienced perspective"-esque articles is already old.
> I have a buffer, that I want to mutate using pointers
Turns out you were right.
Me, I muck around with Haskell. No I'm not productive in it. I don't think that's Haskell's problem, though.
The problem is that instead of a general technique for solving borrow checker issues, every issue has a different specific technique. Hash map matches (like the article) require the entry API. Multiple mutable pointers into a buffer require either using a typed Vec or Vec#split_off. Cyclic data structures require an Arena or Rc<Refcell<>>. String casting issues sometimes require .map(|x| &x). Global state requires lazy_static. Nice error handling requires error_chain. When you run into problems with "Send" you need a Mutex. The list goes on...
It's like pure lazy functional programming in Haskell in that unlike other languages you can't just pick it up based on previous knowledge and patterns. You need an entirely new toolbox of pure algorithms and data structures, or in this case borrow-safe algorithms and data structures.
Don't you think C/C++ are hard to learn too? C++ has a vast surface area and I think it's unlikely that most people who use it understand much of it. When I code in C, I feel like I have to be paranoid because it's so easy to forget a path that doesn't recover allocated resources. But if I'm not paranoid, I just leak the resources and don't care about it (ever, or until many months or weeks later when the problem's discovered). So the easiness is illusory IMO. It seems to me that all of those other resource leaks are just leaking too slowly for us to discover yet.
> Each project involved tons of compiler fighting that I now know how to avoid or solve.
As frustrating as it is, it's easier than struggling with heisenbugs. Those can easily eat up days trying to reproduce issues.
> Cyclic data structures require an Arena or Rc<Refcell<>>.
Is there a way of creating a cyclic data structure that doesn't involve these types?I ask because I used the latter recently, after struggling to create lifetimes that were acceptable to the compiler. I only found it when I searched for 'cyclic data structures rust' on Google.
It would have been much nicer if the compiler could have noticed that I was trying to create a cyclic data structure and then have informed me on the different ways of doing so. Perhaps a misunderstanding on my behalf, but the error messages that I previously received felt like they were sending me on a wild goose chase.
I still don't think writing Rust code is hard, but there are definitely improvements which could be made to the compiler error messages.
But do you learn to code in a different paradigm where you don't struggle by iterating on a compilable and runnable piece of code, but rather struggle on iterating on a non-compilable piece of code and "once it compiles, it works"?
This was the exact same thing I kept reading back when functional programming was becoming in vogue - people were pushing for Haskell, saying that purity and laziness were amazing, type system could almost prove correctness at compile time, etc. etc. You just need to meditate deeply on category theory and draw direction graphs to become one with types and all would be revealed to you.
Hybrid functional languages got adopted in the meantime (F#/Scala/Clojure come to mind) and a lot of FP was adopted by OO languages - it wasn't pure or fancy but it solved problems in an accessible way. Most people still don't use Haskell.
I'm hoping Rust doesn't go down this path as I want a replacement for C++ badly. But saying "you need to spend a few months to get intimately acquainted with the language" is just not going to work in practice if you're hoping about breaking in to the mainstream.
So that's opposed to C++'s "It takes a few years to get acquainted to the language enough that people would want your stuff in production code"?
Nobody hires junior C++ programmers.
In order to just use Haskell you don't need to meditate about category theory at all.
Some things do require months -- or even years -- of studying. Like, for example, C++, Java or whatever was your first programming language. There is absolutely no getting around that.
Haskell suffers more from being a research playground, bad documentation for both the language and it's libraries, confronting you with many unusual concepts (type theory, monads, laziness,...) and, as a pretty old language, a lot of legacy cruft that makes a lot of things awkward (string handling is the prime example).
Rust wants to be a widely used language and while it has some aspects that are not trivial and quite often make it verbose and awkward, in reality it's not really that different from, let's say C++, apart from the borrow checker and lack of classes.
I don't think much will change about Haskell, but I have hope for Rust that it will steadily improve.
I never really saw that in Haskell and I've heard people talking about it for at least 15 years. That indicates that something is missing.
You're right that there needs to be a path up to there though. The reason Scala really worked for me is that it let me make my way up to a very Haskell-like style while remaining productive every step of the way.
But saying "you need to spend a few months to get intimately acquainted with the language" is just not going to work in practice if you're hoping about breaking in to the mainstream.
You mean it's okay for everyone to struggle to work with JavaScript for years and years trying to come up with an OO system and a module system and trying to just bundle the files for the browser, but it's not okay to spend months on getting intimately acquainted with the language?
Who cares if Rust becomes mainstream, we only need the best open source code to use it (like Firefox and the other C/C++ libs that are thinking of porting over). We don't need it to be mainstream, we just need it to be possible to learn it within a reasonable timeframe (less than a year).
Luckily, Rust's community is much more pragmatic - but their goals are still to create something substantially different from existing languages because existing languages are not enough to guarantee what Rust guarantees.
Besides, as a Python trainer I can make you productive in it in a few days. But it will still take a lot of time for you to be good at it. The only difference with rust is that the first "reward stimuli" comes later down the road because you need to be better at it than at Python to make use of it.
In my experience, most developers can't or won't put in this sort of investment. Which leads me to wonder what fields/niches will Rust land in?
This might change if, as other commenters have stated, it is taught as a first or second language, but that doesn't seem likely anytime soon...
This is interesting. Many universities teach Java, C, C++ or Python as a first language. The headache that many first year students suffer from these basic (relatively speaking) languages is readily apparent. Rust is very strict, and it requires an entirely different mindset to use. I wonder how this would fare with students who have a clean slate.
when a language is hard to learn, the compiler error
messages are probably just bad in terms of
comprehensibility and offer the user no direct solution.
I don't want to sound snarky but Rust really does have amazing error messages, and they literally do offer direct solutions/suggestions using literal examples and raw samples of your source code.It even offers the `rustc --explain EXXXX` (where XXXX is an error code) that give an in terminal deep dive + examples on why this is an error/how it is failing.
I never wrote a single line of rust
:|Usually you construct the pipeline to switch between these as you need. If you need to share the result from one mutation to the other, you can do it by passing a simple immutable value that communicates the state change to other mutation, but don't try to do both at the same time. This is simply a good practice enforced at compile time. Of course, sometimes these rules prevent some valid cases, but overall it is worth it.
If we are talking about HashMaps, the Rust std lib has useful Entry api to for get-or-insert use cases. In rare cases where you need to share the value between different parts of the system, there are primitives you can wrap your value in and enable reference counting, as well as internal mutation (even if wrapper is immutable, it provides safe api for mutation). In cases where the concurrency is needed, there are mutexes, channels, and multiple libraries to parallelize the code (like rayon or crossbeam).
let mut owned = vec![1, 2];
let x = &mut owned[0];
let y = &mut owned[1];
That this fails might come as a surprise to a lot of folks because it's easy for a human to see that it's perfectly safe. Namely, neither `x` nor `y` refer to the same point in memory, but the borrow checker still forbids it.What's the solution to this problem? Well, it depends on what problem you're trying to solve. One popular approach is to change the representation of your reference from a pointer to an index:
let mut owned = vec![1, 2];
let ix = 0;
let iy = 1;
Now, in order to dereference your reference, you need to do `&mut owned[ix]` instead of simply `{STAR}x`. There are various trade offs to this choice of course, including the fact that `&mut owned[ix]` will likely be bounds checked. (Bounds checking could be turned off by using `unsafe { owned.get_unchecked_mut(ix) }`.)But there are more solutions to this problem! Here's another:
use std::cell::Cell;
let owned = vec![Cell::new(1), Cell::new(2)];
let x = &owned[0];
let y = &owned[1];
x.set(10);
y.set(20);
println!("{:?}", owned);
This uses a concept known as "interior mutability" to permit mutating through a borrowed reference. `Cell` only works for `Copy` types ("plain old data"), whereas you'd need to use `RefCell` for non-copy types like `Vec<T>` or `String`.Finally, we can come full circle and use the `split_at_mut` API on slices to get two mutable views into the same slice. This is a good example of something that is ultimately implemented using `unsafe`, but exposes a `safe` interface:
let mut owned = vec![1, 2];
{
let (xs, ys) = owned.split_at_mut(1);
let x = &mut xs[0];
let y = &mut ys[0];
*x = 10;
*y = 20;
// The added scope is used so that the mutable
// borrow of `owned` is done before borrowing it
// again to print its contents below.
}
println!("{:?}", owned);
There are various trade offs to each of these approaches and which one you use depends on the problem you're trying to solve. Of course, the criticism of "I just want to use mutable pointers dammit" is valid, because Rust will, for the most part, force you to think of another way of solving your problem (unless you're OK using `unsafe` and raw pointers). We occasionally pay a price for it when the borrow checker isn't quite smart enough. In the example I've shown in this comment, it's obvious what the borrow checker should do, but in a real program, it's rarely so simple.However, what you're asking for is very similar to what types like `RefCell` and `RwLock` do - they effectively delay borrow-checking until runtime. Now you can have references to a `RefCell` in multiple places in the code, and they can all get mutable access to the interior, just not at the same time.
Having said that, I find it interesting how rarely things like `RefCell` are needed in practice: there's almost always a better way to satisfy the borrow checker, and it usually results in much cleaner code.
I think there are two solutions to that problem. The first is to involve HCI people in the development in the language to see how they can improve the output and communication from the theorm prover to the programmer. I imagine that, some time in the future, a very powerful therom prover will make its way into a "general" language and will bring some cool effects. The theorum prover I'd like to see will attempt to prove all of the conjectures used in the program and, when not possible, will say "possibly invalid code. Please solve this conjecture" where then this would be given to a mathamatician and they have to complete the missing steps to see if it works, if not we tell the compiler this isn't going to work and it now generates errors for this.
The other solution is to limit the shared state of every program using very strict segregation of all moving parts and an attempt to limit use of side-effect-causing operations.
This second one is nothing new, I mean the original LISP crowd had these ideas down and theres nothing new.
I don't think I've ever worked with a compiler that has a built in theorum prover, possibly a shared library of proven work, and a way to in the language aid the prover. For instance this type of function definition
int(int numerator, int denominator) where (denominator != 0) divide {
return numerator / denominator
}
And even more so I've not heard of unobtrusive theorum provers that keep out of the programmers way while still being useful. I think an unproven relation should be a warning. It's not good but the program can still run and you (you being not me but the math-people) can come up after me and prove all of my work or tell me I'm an idiot and to fix it.> If you can formalize a better model you can probably implement it in a language, but what model would that be?
I'm just someone on the internet here to complain. I can't do that because I'm not a smart math person who can prove how to get there or even generalize it to work with other programming related things.
You can't in, say, SQL, Rust, or Lisp. That doesn't make them a priori harder than other languages, it just requires a shift in thinking, whereas if you're jumping to say, Java after C++, you can get started right away and hopefully pick up idiomatic Java as you go along.
Instead of thinking "here's how I would implement this in C++, now how do I translate that to Rust?" you have to cut that part out and think "how do I write this in Rust?" I mentioned SQL because that's where that really, really, clicked for me: if you ever write SQL and think imperatively, you are doing it wrong, you have to think in sets, and once you do that, SQL is the easiest and most natural thing in the world. The Rust Book is pretty good, but I think it would really benefit from an in-depth section early on explaining how writing in Rust requires a paradigm shift in your thinking, and exactly how to think Rust-ically. I'd write something here, but I'm still learning how. :)
It doesn't explicitly have a section like this, exactly, but there are several "thinking in Rust" style chapters towards the end.
But I agree with your premise, certainly.
To quote "Will": "On the bright side it is possible to implement a pretty elegant solution to this particular problem"
(You should not read my OP as a negative critique. I'm merely pointing out that anyone expecting 'immediate gratification' with Rust is likely to be disappointed.)
> the compiler thinks about it for me.
Indeed. One valid approach to categorizing programming languages is by considering upfront vs amortized cognitive load.
Plus if one completely ignores pieces like this then they are more likely to fall into a rut where pain points like this are forgotten or ignored completely. While the actual pain of getting used to the Rust way of doing things may be harder to mitigate perhaps there is some insight in opinions like the ones presented in the blog here which can lead to a way to help people get over that pain in a quicker/less painful way.
This will get shorter as tooling gets better this year.
First weeks in Haskell as an average joe programmer you'll spend getting your brain wrapped around lazy eval being the default.
It's hard because now you're in charge of building a malloc(3) implementation. Depending how important this data structure is you might need to worry about 1. about fragmentation 2. free space managment 3. adjacency and cache effects
So you've traded on hard problem for another hard problem. Not something I'd call a win.
You're saying that like those things are a contrast, not two incarnations of the same issue. Safe-and-usable is the holy grail for exactly this reason - right now we have usable-but-unsafe. Javascript isn't a few months to get acquainted: it's a few days to get passable results and a lifetime to get good ones.
So there's a huge need for a better mainstream tool, because the alternative is letting the existing ones endure. That doesn't have to be Rust - there's plenty of space for languages that are hard, but good for key applications like safe browsers. But it has to be something - lecturing people doesn't actually reduce reliance on easy-to-learn, hard-to-secure options.
However Rust is already picking up steam in Firefox modules, hobby OS development, university CS degrees and GNOME modules, so there might be a path there.
I think Swift could play a decent role here if it ever gets good android support as it already enjoys first-class support on iOS. I also agree in that Swift feels very nice. :)
I suspect that the % of students who have difficulties won't be much different, overall. There may be a different distribution of when they run into trouble.
The interesting question is whether the group of students who have problems with other first programming languages is different from the group that have problems with Rust-as-a-first-language...
So you get to drop all safety often instead to have shared mutability.
First, C++ programmers in aggregate have to consider more than the choice of language for their projects. Odds are they work for somebody else, on a team, an established project, whatever. In these cases "C++ programmers need better tools" doesn't mean "toss the bath out with the water." Rust isn't necessarily a good move in established products, in other words.
Second, Rust would do better, in my opinion, to target folks who are new to systems programming and, for whatever (mostly invalid, in my opinion) reasons, are intimidated by C or C++. Rust is very powerful and in many ways I like it much better than C++. I think Rust offers a great way for these people to have a gentle introduction to the world of systems programming. I say "gentle" because a good portion of the "pain" of learning is front-loaded into getting a build, rather than back-loaded into futzing about with gdb (although that would be something these folks will miss out on--who doesn't like a good session with gdb with a satisfying bug squash at the end?). Get them while they're young, as they say.
The overhead for existing C++ teams is learning the language, and (potentially AFAIK) interfacing with C++ code that has no good C style API. But Rust seems to tackle even the latter quite well. I don't think you'll get around the learning curve for a new language.
This is my biggest frustration with the language: the manual is good for what it is, but it only covers the basics. You can see this in the article, where the author tries to implement a simple DNS cache and fails miserably. He doesn't even get to the part where you have a separate thread managing the cache and expiring old entries in the background to avoid unbounded growth and stale data. This is the sort of thing I could whip up in literally half an hour in C, but I just know the borrow checker is going to be a pain in the ass if I try to do something similar in Rust.
Learning curve is a feature for sure, and the Rust community seems to be aware of this. But expectations of minimal effort are odd.
Actually, not jargon. I had Matthias Felleisen's characterization of expressiveness [1] in mind (incidentally, he's also one of Racket's authors).
> Do you have any substantive evidence that "good" typesystems don't reduce it?
If we want to be precise, there is a continuum from statically to dynamically typed language. A dynamically typed language is, after all, the same as a static language with just a single polymorphic type. This means that we can trivially translate a dynamically typed program into a statically typed program, though obviously we don't gain anything by that at this point (this is essentially an argument that Bob Harper has advanced before) and we pay for it in additional verbosity. However, in practice we know that we don't need all that flexibility and can constrain types more, allowing us to get better type guarantees; static types then also allow us to do things that we can't do in a dynamic language (such as compile-time overloading).
[1] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.4...
> This means that we can trivially translate a dynamically typed program into a statically typed program, though obviously we don't gain anything by that at this point (this is essentially an argument that Bob Harper has advanced before) and we pay for it in additional verbosity.
This is exactly the opposite of the argument I advance below; you're claiming that there is a "local" translation from dynamic to static. And on reflection, you might be right, but I'm not entirely sure. It hinges on exactly what counts as "local". Translating away exceptions, for example, requires inserting case-analyses which propagate an exception if one has been thrown literally everywhere in your program. It's "global" in the sense that it touches every node in the program; it's "local" in the sense that the transformations performed don't involve any global knowledge or analysis. I'm rereading Felleisen's paper now to see how precisely he defines things.
ORIGINAL:
> I had Matthias Felleisen's characterization of expressiveness [1] in mind (incidentally, he's also one of Racket's authors).
Great! I believe, by that definition, your assertion that "good" typesystems don't reduce expressiveness is false, at least for common type systems such as Haskell's, Java's, ML's, etc. There are plenty of programs which require non-local rewriting ("a global reorganization of the entire program") in order to appease the typechecker.
A simple example can be adapted from the expression problem: suppose I'm writing an compiler for a simple language, so I have an AST datatype and some functions on it. Then I add a pass to my compiler that removes a certain feature from the language I'm implementing - a certain branch of the AST. Now I wish to simplify my backend by removing the case which handles this branch of the AST. I cannot do that without either dynamically failing in that case (which a type system might even prevent me from doing) or introducing an entirely new AST type which does not have that branch - but this could require duplicating or else considerably generalizing existing code that operates on the AST (a global reorganization). Of course, a sufficiently powerful typesystem might let me do that, but for any typesystem you name, I suspect a similar example can be procured.
To get more precise, the "feature" we're considering adding is "ignoring the typechecker and running anyway". For example, GHC's -fdefer-type-errors. The question is "can any legitimate program written with -fdefer-type-errors be locally translated into an equivalent program that typechecks without -fdefer-type-errors". Without reaching for an explicit escape hatch like unsafeCoerce (which is analogous to "unsafe" blocks in Rust), I think the answer is no.
That said, with regard to your "kill a branch in your AST" example, you can kill a branch with Void:
import Data.Void
frobnicate :: Either a Void -> a
frobnicate (Right x) = absurd x
frobnicate (Left x) = x
Now you can rely on no-one accidentally handing you a Right expecting you to do something reasonable with it.Haskell is not very good at extensible runtime polymorphism (one of its weaknesses). Try OCaml's polymorphic variants.
I'm not trying to make a very nuanced claim here. All I'm saying is that there's clearly a lot of people who struggle while learning Rust, but that this is very different than paying an eternal development cost similar to that struggle. We should be careful not to conflate them.
(I did not meant to get into a broader philosophical discussion about using a type system. That's a different conversation IMO, and I disagree with your analysis because you only focus on the upfront cost while neglecting the maintenance costs in the absence of such proofs.)
Much easier than what? Writing C? I believe you.
I agree with you about maintenance, and I agree that there are two separate issues, but still, extra proof must invariably carry some extra mental burden. There are no free proofs just as there are no free prime factors. This isn't a philosophical issue, but a computational one. But it's also true that the mental burden might offset a different sort of mental burden. So whether there is additional work or not is unclear, but I think it's pretty clear that the nature of the work is different.
I'm not going to debate the trade offs of type systems with you. It's been done to death.
I tend to agree with this sentiment. The moment I saw that OP was doing pointer arithmetic: I knew they were in for a bad time. It's not that you can't do it -- it's moreso that Rust's raison d'etre is to highlight the flaws inherent in that approach.
I hadn't thought about it, but I think you're right, my background in managed languages is probably why I didn't run into these sorts of issues until after I was already comfortable with Rust. I wasn't trying to make maximally storage-efficient collections right out of the gate, instead I was building application-level stuff on top of `std::collections`.
One obvious argument is that Rust frontloads its difficulty. It's a terrible tool for most 30 line tasks, simply because the majority of your work will go into arguing with the compiler. But that's true of a great many tools - if there's an adequate payoff in safety and comprehensibility at scale, they can still be worthwhile.
That doesn't solve the learning-and-usage problem, though. Most of us are aware that the explosion of dangerous-at-scale tools like Javascript is largely because no one adopts a language that's unfriendly until you reach enterprise sizes. Even if Rust is "worth it", it's unlikely to catch on while the learning experience feels like pulling teeth.
You can't tell from the type signature alone if a pure function uses ST though, so I'd say that counts as pure :)
Trees, indexes (including bitmaps), buffer allocators and even just keeping state in streaming cursors (Volcano model). Are all pain in the ass in to implement.
In fact implementing a Cursor trait for like 10 different Cursor (SrcView, Join, Project, Compute, ...) became painful because each cursor struct has it's own complex lifetime params and those lifetimes were trying to leak into the main Cursor trait.
Right now I can't do that without resolving to runtime using RefCell. In my case the plans could be entirely materialized at compile time, so not really a win.
The situation won't get better until there's some kind of abstraction over lifetimes (HKT?).
;; Giving names to abstract things a little
(deftype zero () `(eql 0))
(deftype non-zero () `(and number (not zero)))
;; Declaration
(declaim (ftype (function (number non-zero) number) divide))
;; Definition
(defun divide (n d) (/ n d))
;; Test
(lambda (u v)
;; introduce a type constraint here.
(declare (type zero v))
(divide u v)) ;; (nb: underlined in orange)
This message is also printed: note: deleting unreachable code
warning:
Derived type of V is
(VALUES (INTEGER 0 0) &OPTIONAL),
conflicting with its asserted type
(OR (AND NUMBER (NOT INTEGER)) (INTEGER * -1) (INTEGER 1)).
See also:
SBCL Manual, Handling of Types [:node]
The warning happens when two types are and-ed (intersection), and the resulting set is empty. You still allow code where there is potentially "bad" value flowing from one place to another, because ultimately you know the runtime will detect it. In other words, the goal is to reject false negatives (unwarranted warnings), which contrasts with statically typed languages where you only accept code that is guaranteed to satisfy static typing rules (it makes sense there because the compiler is your last chance of catching those type errors, since the runtime does generally not perform type checks).(edit: actually use non-zero instead of (not zero))
Edit: I want to clarify. I think just type checking should be written as "Just type checking".
I am sure they want that. But so many responses of Rust enthusiasts to any article/blog less than flattering get very aggressive. May be Steve Jobs got away with 'You're holding it wrong' but I doubt incoming users for Rust will be this kind.
The Rust thesis is: "You should care about all these things you don't currently care about, and Rust helps (makes) you care about them." In order to enjoy Rust, you need to thoroughly buy in to that idea. Any significant deviation from that thesis defeats the very purpose of the language.
But of course most people aren't going to buy in, because it's an idea predicated on making their lives harder (in the short term) than it currently is. Rustaceans believe the long term benefits are massive, but new users cannot possibly believe that except by taking it on faith. This creates a conflict. The Rustaceans need everyone to give the language faith over an extended period of time, and anyone giving up their faith after a short trial not only drops out of the ecosystem themselves, but possibly damages the faith of others. That's a huge problem, because Rustaceans are hoping their language will one day take over a significant portion of the systems/embedded development space. That can't happen if people sour on the language so quickly. So when Rustaceans see someone slam their language with giving it an "honest try," they get upset because the damage done is palpable.
Of course, that's the wrong strategy, as it just drives people further away from the language. The right approach is to be inhumanly kind and accepting so that the frustrations are counterbalanced by support. The good news is that the Rust community at large seems to be quite nice; it's just the angry ones that show up in blog comments.
Take as an example call/cc. If I tried to explain that to a--as you put it--blub programmer, I would probably get glazed eyes in response. If I instead tried to explain it via means of a yield operator (à la Python), I would much more likely get someone who could think of use cases where they would want to use it in their own code. The limitations are not in the blub programmer, it's in the teacher for assuming that blub programmers are incapable of learning hard concepts.
It's a huge entry barrier.
Which is what's being discussed.
To me it's not a problem at all, Rust is working exactly as designed. When you start writing code in the style that Ayende has: its memory layout becomes entirely non-obvious. The structure definition no longer lines up with how that memory is actually being used. You'll need documentation of the sentinels & invariants, you'll want diagrams of the memory layout (since it won't line up with the structure definition), and a careful understanding of every line of code packing the bytes -- that's just to get a basic understanding of what's actually going on.
This is fine, and you absolutely need to be able to do it, which is exactly why Rust provides `unsafe {}` (along with `std::ptr` and `std::mem`.) Sometimes you need that control you want a memory-efficient data structure, other times that's because you're reading registers directly off the hardware. In either case Rust is going to make you use `unsafe {}` which is a signal to say: "to understand how this is really laid out in memory you need to understand every single line in this block."
I would much rather be able to grep for `unsafe {}` and audit parts of a trie-map for memory safety, as opposed to auditing my entire program for memory safety violations. That's not a failing of Rust, it's functioning exactly as intended. Trying to squeeze every last byte out of a struct is neither obvious nor safe, it requires careful thought.
The code in the blog post looks very much like C-translated-to-rust. Right now, systems programming mostly does look like C/C++ because those are the only two mainstream languages that work here (D, too, but D is like C++. There are also other languages). But systems programming doesn't have to look the same or use the same primitives.
Do you really need to use malloc to allocate memory in a systems language? Do you really need to use pointer arithmetic to deal with things in a systems language? Sometimes, but not necessarily often. Even the Rust operating systems have managed to minimize the amount of unsafe/C-like code.
Rust does expose this functionality, but if it's not something it expects you to use often it makes sense that it might not always be straightforward to use.
This arguing with the compiler is really only while you are learning Rust and it's nuances. For me the first two weeks were this way, then I started thinking more in Rust terms. Now I rarely deal with the borrow checker at all, but I do end up in some complex type situations.
You're spot on with the frontloads difficulty. Honestly though, I can't think of any language that I've learned that in under a few weeks I knew how to properly do things in the language.
Python was this way for me. My code looked like C when I started with it, and then I learned after a few weeks about 'with' and 'yield' and started writing real code.
Rust is worth the initial investment, but the first two weeks will require some perseverance.
It just means you've learned to head off problems before they occur, that does not mean the problems you're heading off should be there.
It does. Ada did too with people saying same stuff. Once they got used to it, they swore by Ada since their stuff usually worked after they got it past compiler. I also add that the alternative to Rust for GC-free safety of heap code is essentially separation (VCC) or matching logic (KCC). That's probably much harder for average programmer to work with than structuring stuff to get through borrow checker easily. Cyclone and then Rust were huge steps up on dynamic, memory safety given they're accessible to people who don't know formal verification.
That's the comparison people keep leaving off griping about borrow checker. I think Rust looks a lot better when one adds it. Still a headache but good in relative way.
No, unsafe would be overkill and if you needed unsafe, it would put Rust at a massive disadvantage vis-à-vis garbage-collected languages.
Simply put, Rust doesn't handle multiple ownership well. It results in syntactic bloat and/or runtime overhead. But multiple ownership occurs naturally in a number of real-world programs.
> Second, this article only considers the costs of a stricter borrow checker, not the benefits.
This is why I very specifically mentioned that it's a tradeoff.
Destroying a circular list takes some thought to make sure you don't double free, same as Rust. Rust just makes those sections of code very explicit and calls your attention to it, while in C you may end up with a segfault, or worse a program that only shows odd bugs during runtime. At least Rust is telling you that you need to put some extra thought in this section.
I don't see an issue with 'unsafe' if it's an implementation detail and well-tested. Obviously it would be better to avoid it if possible. 'unsafe' is overkill sometimes and worth it other times.
unsafe should be IMO, reserved for only those cases where strictly needed, e.g. FFI is the clearest example.
The same can be said of C and C++, at which point, why does it matter if you use Rust?
It certainly takes a lot more typing to get things done.
It's much like visiting other, radically different countries than the ones you've been brought up in. Even if you don't wind up moving there permanently, you'll be richer for the experience.
On the other hand this defeats the goal of the language - being as safe as possible - because their domain is very sensitive to memory safety issues (browser) - promoting an ecosystem of unsafe libraries wrapped in safe facades would be against their goals.
So I guess right now, because of it's design decisions, Rust isn't simply an upgrade to C++ which I want but more of a language that took different trade-offs to solve specific problems. It remains to be seen if it's worth taking those trade-offs to be rid of C++ legacy garbage.
I have pushed this several times in the past, but there's always been a lot of resistance to the idea. Though I've always phrased it as "let's allow the safety checks to be turned off", which isn't necessarily the best phrasing...
I wonder if it'd be better to just push to make unsafe easier to use: add sugar for ptr::offset, add a -> operator, relax the restrictions on casts, and so on. When I do OpenGL programming in Rust I drop into unsafe quite regularly, and it feels just like C, except for some annoying papercuts. It'd be nice to get rid of those papercuts.
Make it easy to reason about useful kinds of partial safety. Make it easier to define properties of external systems that Rust does not control.
I find passing ownership around very usable, and can never understand how the bit-twiddling C folks ever get anything done. Maybe that's my mathematical background - I like my functions to be functions.
> I'm just someone on the internet here to complain. I can't do that because I'm not a smart math person who can prove how to get there or even generalize it to work with other programming related things.
The hard part is forming the model. Once you actually know how it's supposed to work, proving it is the easy part. But I mean, how would you even explain to another human that a given algorithm is safe, if not by arguments along the line of "look, this function owns this piece of data and this function owns this piece of data and they're called one after the other"?
So what's still unclear to me is, is this failing in the borrow checker "by design", or is it something that will be "improved"?
Also, _why_ does it fail in the borrow checker? Is it correct that it fails, or is it a deficiency of the current borrow checker?
I only posted samples to demonstrate working with the borrow checker. Think about this code:
let mut owned = vec![1, 2];
let x = &mut owned[i];
let y = &mut owned[j];
What are the values of `i` and `j`? If they are equivalent, then this code is unsafe, because it would permit mutable aliases. Therefore, the borrow checker would need to prove something about what values of `i` and `j` are legal at this particular point in the program. (Errmmm, dependent types anyone?)With that said, if you do have code like my original sample (where the indices are constants and trivially not equal), then perhaps it would be reasonable to say that is a deficiency of the borrow checker as it currently exists that could be feasibly fixed. It's not clear what the impact of fixing that deficiency would be. For example, I don't think it would have made the OP's life any better.
It could have special cases for many things, such as letting the disjoint indexes pass. But that would be a special case because it can't always prove that the indexes are disjoint. So it leaves up to libraries to provide APIs that do that. This is why `split_at_mut()` demonstrated in grandparent post exists.
One example of how this could work is writing a spinlock. Let's use a made up language for now and adapt that to our need.
alias lock unsigned int;
lock() make_lock (
return 0;
)
void(ref<lock> l) take_lock (
while (l != 0); // Assume this is atomic for brevity
l += 1;
)
void(ref<lock> l) give_lock (
l -= 1;
)
This is a "correctish" non-reentrant lock. This has many vectors to see if this is "correct" from a compiler standpoint. The first is that only the alias "lock" can be passed to my lock even though it is just an "unsigned int". Another mode is that I am doing (lock -= 1). Since this is unsigned if my program ever gets into a state where I am doing (0 - 1) in unsigned arithmatic then I am going to wrap around to UInt.MAX_VALUE which is not how subtraction should behave and it should warn me when this situation arises. If the language allows me I can also do this to help the compiler know that I never want this to happen. void(ref<lock> l) where (l != 0) give_lock (
l -= 1;
)
Now the compiler can see I absolutly do not want this wraparound behavior and it can check for it. Where would this be the case? int() thread_safe_fac(ref<lock> l, int some_important_data) (
int f;
take_lock(l);
if (some_importand_data) {
f = some_important_data * thread_safe_fac(l, some_important_data - 1);
} else {
f = 1;
give_lock(l);
}
give_lock(l);
return f;
)
void() main (
lock l = make_lock();
int some_important_data = 10;
some_important_data = thread_safe_fac(&l, 10);
print(some_important_data);
)
Will this code work? Absolutly not and it's easily checkable. One of my function's invariants is violated. give_lock() will sometimes fall negative. This is a very crude example but I've seen code like this in larger projects where someone modified some code and someone refactored that then someone was contracted to add a feature. It just goes down the shitter. But this is easily provablely wrong. * Call thread safe fac
* grab a lock
* we theoretically touch some thread safe data
* We hit a case where we do an if
* Then run the then case
* else set a default and free our lock
* Free our lock
We've free'd twice and we've now broken the underyling data of the `lock` type. We've got an unsigned int that has been wrapped around to an huge number and take_lock(l) will never work again. Lock is a mutable datatype that can be shared between N many threads/fucntions and it can always be checked like this just by telling the compiler "hey I never want to let the internal int fall into a state where it is subtracting 1 from 0".This can also be extended to other things
void test(int *num) { *num *= 10; }
int some_complex_function(int *num) {... free(num) ... }
int main() {
int *some_cool_data = (int*) malloc(sizeof(int);
*some_cool_data = 5;
while (true) {
...
test(some_cool_data);
...
some_complex_function(some_cool_data);
...
}
}
We've come to a place in the code where we have a possible path of execution (regardless of the depth in if statments or gotos or other control flow) where we will eventually come to the point where we use a bit of data after it's free'd. This code will be correct if you do something like. // We now make sure the compiler knows that we only want to accept real references to initialize ptr
int some_complex_function(int **num) where (num && *num) {... free(*num); *num = NULL; ... }
// We've removed the path through the program that allows the data to be used after free.
while (some_cool_data) {
...
some_complex_function(&some_cool_data);
...
}
Again another bad example but there is a lot of perfectly valid, and very clean code that is against the borrow checker but is still completely correct.You can use e.g. Nat in Idris to avoid the underflow possibility. A language shouldn't allow silent overflow, and to the extent that Rust encourages a style where you fiddle with overflowing integers this is a problem with Rust, though I don't believe Rust does encourage that style - it provides locks, and if you were writing a custom one for some reason you wouldn't naturally fiddle with an int, you'd use an affine type or some such.
It would be preferable if you could make your own slice-like types which supported indexing, but to do that you need a mechanism to tell the compiler that your `IndexMut` implementation returns disjoint references for different indices, and that requires a mechanism for the compiler to determine whether two indices are the same (in case your indices are not integers).
you mean auditing the entire implementation of the trie data structure, not the entire program.
Guess what?
In rust, if you use unsafe, you also have to audit the implementation of the entire trie datastructure.
And if you think you don't, then I submit you don't understand Rust safety that well.
The parent post specific said it wanted a replacement for C++; the amount of time and effort it takes to get code right in C++ is a problem.
Rust also has a long learning curve, and its hard to quickly be productive in.
I don't think there's anything wrong with idly wishing there was some kind of middle ground where you could gradually or partially opt-in to the borrow checker with a nice modern language like rust to get started, and then gradually eliminate your GC parts or whatever.
Many game engines use a high level language with a GC like C# or (shudder) python or blueprint which are baked by C++ for the performance critical parts.
You write application logic in the scripting language, and anything that's too complex you fall back to C++ for.
If rust could somehow self host itself as a high level language like that, it would be superb.
It may stop Rust from getting widespread adoption, but I see no problem with that. C++ isn't "widely used" either by most meanings. Rust may still largely replace C++ even without magically having a less steep learning curve. (Which it, IMHO, actually has anyway.)
As someone who learned C++ first and later Rust, no it isn't. Rust's learning curve is a hell of a lot higher than C++'s.
This reminds me of when Java was trying to compete with C++, you would have all these Java people making claims that HotSpot was going to take over the world and make Java faster than C/C++. Only it never happened outside of a few specific cases because these people were "fanboys" in every sense of the word.
You like Rust, there's nothing wrong with that. You think it's better than C++, that's your belief and I'll defend your right to believe it.
But stop comparing it to C++. Just about every language over the past 20 years has users do the same thing and __C++ IS STILL KING__.
You can push rust without being unfair to C++ (NO WAY is C++ harder to learn than Rust).
I think those will be trivial in that direction, harder in other - but that's not even relevant. I think if Rust aims to occupy C++ domain then it needs to be very easy to get C++ developers on board - and from what I've seen so far it's not quite the case. I'm not sure what can be done about it tho - maybe emphasize unsafe is not a dirty word but a valid approach when doing low level data structure stuff like OP ? Create an unsafe version -> refactor to safe as you're sure it does what you expect and you learn more about borrow checker instead of having to absorb it all at once.
People who complain about how difficult rust is to learn certainly aren't learning rust as their first programming language.
Because they are required to be productive fast. A professional usually doesn't learn a new language just for the sake of it. If the language doesn't yield instant benefits then most people aren't going to bother learning it.
If a steep learning curve is expected for rust then the language will never be popular around the people who could potentially make use of it, i.e. C or C++ developers.
E.G: one of my friend has a streaming website. He make banks with ads despite the site having problems all the time. He doesn't care the least. He is not an expert coder and just want to be able to build the features he needs, even if it's not perfect. Types are way too much for him. It took 2 years to master git basics.
Another example: my current clients are geomaticians. They code in Python a lot. They produce only small codes, and don't want something complex. They are experts in maps, not code, and love Python to have the balance of cost / reward they want.
HN is a bubble of expert devs, but the world is full of PHP plumbers, data manglers, sys admin and other professional that have a legitimate needs for "smooth brain to text programming".
Even in Rust's ideal future, very little of the code atop the stack is written in it. It's the code at the very bottom that underpins everything whose share it hopes to eat.
You don't think the OCaml type system would do this? OCaml gives you just as much safety as Rust does, it just doesn't also give you no GC like Rust does. And the price Rust pays for the absence of GC is the lifetime concept, which adds a very significant layer of complexity to the language. Much like OCaml, Swift and Scala also provide very safe type systems.
Again, if you can't have a GC, then Rust is an excellent option, and the only really safe GC-less language unless you use C with FramaC or similar static analysis (and FramaC also requires lots of training and time to use, just like Rust).
Upcoming OCaml multicore solves the shared state and data races problem with algebraic effects[0]. It is working, it's just not yet been released in mainline OCaml. Supposedly, that will happen this year, but we'll see.
0. http://kcsrk.info/ocaml/multicore/2015/05/20/effects-multico...
Which isn't to say OCaml is trivial, or Rust is useless (GC and threading both being limitations). It just makes me skeptical of claims that Rust's difficulty is irreducible.
But you can run OCaml on a microcontroller:
Upcoming multicore OCaml will provide such guarantees through algebraic effects [0].
0. http://kcsrk.info/ocaml/multicore/2015/05/20/effects-multico...
Second, Rust also prevents legitimate sharing in a bunch of cases where concurrency is not involved. (and a simple scoped allocator succeeds)
This would be fixed if there was an easy way to have typed memory that can be shared but cannot be accessed concurrently. There is none. You get at most full unsafe.
I definitely share your concern; for me, Rust was like putting on a glove perfectly shaped for my hand. It works and prevents every basic pattern that it took 10 years to learn and start applying to all my code thereafter. If I had Rust all those years ago, it would have saved me a ton of time... but I probably would have turned my nose up at it, b/c I wouldn't have had the experience to understand what it was saving me from.
So yes, I want the learning curve to be lower, but not at the expense of losing all of the safety it brings to developing fast and reliable software.
If someone thinks they won't gain anything by going from C++ to Rust, then no effort to learn it will be worth it. Obviously the Rust devs think there is a benefit.
The only thing you cannot exactly prevent is "cast_dammit_cast".
Static analysers and code reviews are not always possible, depending on the target OS and compiler being used, or how the whole project across teams is organized.
The point here is that the type system does not restrict me from implementing a data structure that adheres to this constraint, even if it does not enforce it for me.
(Again, not arguing against your thesis here.)
It would literally break forward compatibility.
If you don't need manual memory management, it's obviously going to be a burden. There are still use cases for manual memory management though, and that's where Rust comes in.
Use the right tool for the job.
That's exactly the point that I made in my original comment?
It was, for instance, a way more natural transition than "Java and C++ to OCaml". Less educational, but as usual that trades against easy.
You have numeric pointers into arrays, they're called indices.
OCaml is actually used for many systems programming tasks that can live with a tracing GC.
Many things in tech are tradeoffs. Sometimes the additional tools a language brings to the table are worth the learning curve, sometimes they aren't. There are people in the industry for which FP (or Rust, or whatever) is worth it. For you it isn't. That's life.
C++ has 30+ years of libraries, so does C. To even think you can compare Rust and C/C++ on that matter is not realistic.
That's an example of just two blocks, but if it's deeper structures then ignoring this until the end could cause you to go back and restructure significant sections of code to make it abide by the mutability rules.
The process I use to stay productive, is to write small blocks of code, write a unit test for it, compile, and then move on. That way I don't build up a ton of compiler debt...
Depends on what you mean. Writing good, safe, C++? Quite hard. Debatably harder than Rust, but it's debatable, not an absolute statement.
The difference is that C and C++ let you write things like doubly linked lists in day 1 of learning them, whereas Rust won't. But a good doubly linked list is tricky to get right anyway.
I learned C++ first too, and I think I have a handle on "safe" C++ practices, but it's mostly a nebulous set of conventions that shift in the particulars based on the codebase. OTOH in Rust I'm immensely productive even in random codebases.
Most of the cognitive overhead of Rust exists in C++ too, since you still need to reason about how your data is shared, just that C++ spreads the cost out over the learning curve, whereas in Rust you have to deal with it up front.
I've been programming C++ for 16 years, it's the main language at my day job, and I won't even begin to think I understand templates. But you'll see codebases that use it regularly. Const correctness? rvalue vs lvalue? ODR? SFINAE? RVO? exception safe code? template argument deduction? Then you have C++11, C++14, C++17...
Despite this, I still regularly write code that has memory, string, concurrency and bounds bugs. So does everyone else in the industry, including smarter people with more experience.
It's possible that the parts of Rust I'm not familiar with (e.g. unsafe code, macro system) turn out to be similar sinkholes, but so far, that doesn't seem to be the case.
I had the exact opposite experience, i.e. Rust allows me to not worry about memory safety and allows me to think on a much closer level of abstraction to my problem domain than C++.
Anyone who thinks this is what it means to learn a language is attacking a strawman.
OTOH, many students of programming languages learn by pushing their code (EDIT: ready or not) into production. Oh boy do they learn.
All of these things have a larger cost than Rust: you're forced to use pervasive reference counting and very defensive data structures. Rust allows you to get away with pointers deep into other things, as well as aggressive data structures, all in safe code.
Rust also has reliable use-after-move defense for arbitrary resources (not just vague dynamic protections for only the memory resource), and defends against data races. That is, Rust has true safety in a way that can give higher performance than approximate safety in C++.
To add to this point, my answer would be a big NO, if one would look to the C alternatives that sadly became niches.
Sadly the adoption of C has lead to younger generations thinking that those are the only valid paths in system languages.
Thankfully new languages, including Rust, are changing that.
It is written from "I have one CPU" side. Even C is far beyond that with things like OpenCL...
In any case, Rust is definitely not written with "I have one CPU" in mind. A slogan that is sometimes used for Rust is "safe. fast. concurrent.", and it's one of the headline features on the front page: http://rust-lang.org/ . Rust's abstractions allows writing extremely high performance libraries like rayon, which, had better performance than even highly optimised C libraries like Cilk, in one benchmark I heard about. Building out that parallelism ecosystem takes time, so it does not seem particularly surprising that there are more (unsafe!) libraries for the 40-year-old language, than (safe!) libraries in the less-than-2-year-old one.
If you're adding performance considerations I think the answer is going to be yes.
polymorphism is of course an abstraction, the idea of cost-free is that you only pay for the abstractions you actually use.
This is not the same as maximizing performance.
The real question is whether or not you can do these things without giving the developer the control to maximize performance, and I think the answer is going to be no. The response will be that it's "good enough", but that's not actionable as we don't know what "good enough" means for any particular project.
You can build (or use, from the stdlib) safe abstractions over malloc that have the same performance. This should cover 99% of the use cases of malloc. For the few quirky cases where you still need malloc, you can still use it, but that means that the language doesn't need to make malloc-based memory management easy.
You can't do systems programming without them, what you can do is minimize their use by building abstractions, which other languages, such as C++ and D, already do.
So this idea that people somehow think you have to use the actual malloc call throughout your code is a bit of a strawman.
Have you used C++ in the real world ? Dealt with it's insane build systems, build times, obscure semantics, etc. ? Not to mention there's nothing close to a sane package manager.
There's plenty of value of just rehashing the lessons learned in last 20 years of language design to a systems language.
I don't think language developers are interested in making
such a language in general...
https://inductive.no/jai/:P
I'd argue go came from a desire to have a language that was completely uninteresting in terms of 'interesting ideas', and really just exists to Get Stuff Done.
I think more people are interested in that sort of thing than you imagine. Clearly.
Or more precisely, the designer of Jai uses C++ as "C with classes" and then goes from that, which isn't fair. He also misrepresented a few things in regards to Rust, which he doesn't even know, but comments on etc. etc.
fn foo() {
unsafe {
let x = &5; // this is still going to get borrow checked
}
}
Since it's a superset, it gives you access to new tools, that don't interact with the borrow checker, rather than "turning the borrow checker off". fn foo() {
unsafe {
let x = *const 5; // this is not going to get borrow checked
}
}You can write arenas in Rust.
You cannot implement many data structures in a decent performance way without unsafe, so Rust guarantee is weak.
It is not extreme, and it isn't even necessary: just reuse one of the many arenas others have already written, e.g. https://crates.io/crates/typed-arena .
Copy-on-write is fairly ease to write in Rust, e.g. the even core Rc and Arc types have a get_mut function that does copy-on-write.
> You cannot implement many data structures in a decent performance way without unsafe, so Rust guarantee is weak.
You can't implement anything in C or C++ without unsafe: Rust's main strength is the ability to write high performance code with a safe interface, so that only one library has to muddle through the `unsafe` problems (if they have to touch `unsafe` at all). Most code one writes is not implementing data structures.
As a demonstration of this strength, even very low-level projects like operating systems can get away with not using `unsafe` everywhere by packaging it up into safe interfaces, e.g. http://os.phil-opp.com/modifying-page-tables.html
You get to use unsafe almost exclusively in the internals of such access where it shouldn't be necessary if there are ways to specify more properties than just binary "safe" and "unsafe".
It is extremely hard to prove such code correct in Rust.
The only way I can make this make sense, is having cross-platform abstraction around atomic orderings etc. (like the new C++11 std::atomics) that works on x86 and ARM and PowerPC etc.
Rust has such a cross-platform abstraction, almost identical to C++11: https://doc.rust-lang.org/std/sync/atomic/index.html
Please be more specific about what exactly you mean if I am wrong.
So yeah, (modern) C++ also contests this claim too. Rust contests a stronger claim about safety as well.
------
You're the one who said that "it's claimed to be a systems level language, and if it's really that painful to do systems level things," -- my point is that in both Rust and C++, "systems level things" don't need to involve pointer arithmetic and other similar things. They should be niche tools you reach for when the abstractions fail you, and the abstractions should not fail you that often. This is already true for both languages, I feel. It's not "niche" in C++ because the ecosystem needs time to evolve to that point, but for new code it probably works out.
Then there's the C constructor convention through opaque void pointers idiom.
This stuff is not new, not even close to new, NO ONE, not even the kernel developers, would make the argument that you cannot build abstractions around malloc.
If that's really what you're arguing, well you're not saying anything that C developers don't already practice.
smart pointers have been around for a while too. I see some codebases using them. But many don't.
> NO ONE, not even the kernel developers, would make the argument that you cannot build abstractions around malloc.
I never said that kernel devs or whatever would make that argument.
----------------
Look, you seem to think I've said something different than what I did here.
You said "if it's really that painful to do systems level things, then there's a problem somewhere.". I'm saying that Rust contests the claim that malloc/new/whatever must be easy to use for systems level things. Easy-to-use abstractions around them work well too. It's not that C++ doesn't have these abstractions, it does, and has for a while. My statement isn't one against C++.
My point is that the difficulty of using programming paradigms designed around malloc/new should not be a dealbreaker for a systems language, provided that the same tasks can be accomplished without raw allocation using a suitably generic abstraction. C certainly promotes malloc-based-paradigms. C++ has good alternatives, but a lot of the lower-level stuff I find avoids them because ultimately malloc/new aren't hard in C++. Rust makes them harder to use, but provides sufficient abstractions/abstraction-building-power that this should not be an impedance to systems programming. And, again, I'm not saying C++ doesn't -- I'm just saying that C++ also makes it easy to use malloc/new-based patterns, so you will continue to see those in systems programs even if they aren't necessary.
Now, interestingly, an abstraction for a vec-with-header doesn't actually seem to exist in the crate ecosystem, probably because folks don't like unconditional allocation. But it's relatively easy to build, might do it over the weekend.
(You can fake it in safe code using DSTs but it's not so great)
You may not like that they tend to be very thin abstractions, but they are abstractions, and the point is that the idea of building abstractions around malloc isn't new and certainly isn't unique.
You said:
>> You can build (or use, from the stdlib) safe abstractions over malloc that have the same performance.
While in defense of the statement:
>> Rust contests the claim that you actually need to do pointer arithmetic and stuff often in systems languages. You need to do it a bit to implement your abstractions, maybe, but it is not the first tool you should need to reach for.
This is presented as something unique to rust, which isn't even close to the accurate. This is why I called it a strawman. No one, not even kernel developers using C, are incapable of building abstractions around malloc to make it easier/safer to use.
The rest of what you say is inconsequential in my opinion. This isn't about C++, this is about your implication that building abstractions around malloc is something new or novel about Rust. It isn't. Not even the idea of safety around malloc is new, new[]/delete[] are themselves abstractions intended to make it less error prone to create/destroy contiguous blocks of memory.
That doesn't even get into the smart objects and RAII which have been around for a very very long time in C++.
None of this is new to Rust, and I think it's a bit disingenuous to imply that it is.
Right, I wasn't talking about just that, I was specifically referring to abstractions that make you change your programming patterns. C++ has those too (the move-based abstractions), but I don't consider new/delete to be them.
My point was sort of "being able to write code that looks like this is not necessary for systems programming". Being able to do the task at hand is, but it need not be done using code like that.
> This is presented as something unique to rust, which isn't even close to the accurate.
Fair. It's not. I didn't intend to mean that. I'm sorry.
I was a C++ programmer for a quite a while, some of which time I used modern C++, so I'm quite aware that these abstractions are not unique to Rust.
The reason I phrased it that way is that C++ doesn't really make it harder to use direct-malloc-based (or raw-pointers-with-new) programming, and most of the lower-level C++ code I've seen (this is anecdotal) still uses this. So C++ doesn't contest the claim the same way Rust does (it does contest the claim though), since Rust codebases doing the same thing (even the kernels in Rust) tend to move strongly in the direction of avoiding unsafe pointer twiddling as much as it can. This is a bit more nuanced (and based on anecdotal evidence on low level codebases I've seen) than I really wrote in my original comment, and I apologize for that.