Once you move past the prototyping stage on to full on production coding, I totally buy that in a lot of cases, the safety that Rust provides (if it compiles, it will almost certainly either panic or work, ignoring logic bugs), is a big win, but I'm not convinced yet for experimentation when prototyping...
So, The first draft is going to take like twice as long, but every subsequent iteration on your prototype will be much faster. I find the payoff time to be pretty quick (like one or two iteration are often enough), but obviously if you're doing a true one-shot that you're never gonna touch again, it won't be worth it (that being said in my experience, such situations are pretty rare and most one-shots end up being way more than what they where supposed to in the first place).
C++ is probably at the other end of the spectrum since it's too easy to accidentally cause a memory error during refactoring. C# and Java are a bit in-between, the automatic memory management limits the footgun, but the type system is much less powerful than Rust's one so there still a lot of issues that can come during a refactoring. I have only very limited OCaml experience and no Haskell experience but I think you'd have most of Rust advantages with those, except the thread-safety part which is my favourite part of Rust but is also situational.
(Safe) Rust has Data Race Freedom, which means you get Sequential Consistency. As I understand it, Haskell does not promise you data race freedom, so, if you mistakenly implemented a data race now you don't have Sequential Consistency and now you can't reason about non-trivial programs at all.
I think Haskell, like Java, and unlike C or C++, promises you still get a meaningful program if you have a data race, but it may be tricky to debug the mess.
Edited to expand: Suppose you're writing a program to process 16GB of data, the result will be a floating point value between 0 and 100.0 as a result of this processing. You realise it's embarrassingly parallel and so you spin up 16 threads to solve the problem in parallel on your CPU cores.
Mistakenly instead of processing 1GB per thread, you process the same 1GB on all sixteen threads, the processing involves a lot of twiddling data, and so now it's full of data races. What is the result of your buggy program?
In Rust, the program doesn't compile, or depending how you wrote it, exits immediately reporting that you can't go around processing the same data from different threads.
In C or C++ maybe it deletes all your data, more likely it simply exits instantly with no result, spins forever, or outputs something completely spurious.
In Java you might get an Exception reporting that you mustn't do this, but you might get a bogus but real looking answer like 46.2 now good luck figuring out why.
I don't know for sure about Haskell, but I think you get a similar situation to Java minus the exception.
I don't think this is correct. Safe Rust includes atomics that are data-race free but need not be sequentially consistent.
However you can indeed use it from Safe Rust, and you can choose Orderings which do not give you Sequential Consistency if you're comfortable with that, or more likely because you are implementing an algorithm whose authors assure you this is fine (I hope they're correct). I think that's a very modest caveat to the overall promise of Sequential Consistency, but sure, it is a caveat. If you write code that explicitly asks for Ordering::Relaxed and are astonished you don't get Ordering::SeqCst I guess I apologise for confusing you.
This is true (when you insist on mutable shared memory), but in Haskell the safety guarantee is in fact not so far from Rust. The Haskell community is well aware of concurrency issues very early, and they developed a dozen of composable abstractions to solve these problems. (IMHO Rust is the only one that has a truly novel solution in recent years.)
In Haskell, either you have to use `unsafe` functions to share bulk of memory or you don't share memory at all (as mutability is a pita in Haskell). On top of these unsafe functions, you can build other safe abstractions, just like Rust. These abstractions are often available as higher-order functions in libraries, making them highly composable and reusable.
E.g., the canonical memory-sharing data type is `TVar`, which must be access from the `STM` monad [1], guaranteeing both atomicity and data race freedom. (BTW, STM stands for software transactional memory if you aren't familiar with it.) A major offender is `IORef`, and fortunately Haskell provides atomic operations on `IORef`s.
GHC Haskell also offers free parallelism for all pure computations via the `Eval` monad [2].
> Mistakenly instead of processing 1GB per thread, you process the same 1GB on all sixteen threads
This is highly unlikely in Haskell. Haskell emphasizes on composable combinators, so a program that does not share memory would probably look like
foldr accumulateFunc initValue . mapConcurrently processBatch . split batchSize $ data
(mapConcurrently here is a real function [3].) I'm sure C++, Java, and Rust have similar abstractions, but imperative languages make it too easy for programmers to do `for` loops, making it much more error-prone.Several array libraries provide safe parallelism. The interface is similar to Rust rayon, but the way they work differs greatly. For example, in Repa [4] you can do
computeP $ traverse inputArray newShapeFn (\getElem curIdx -> ...)
Here `traverse` results in a "delayed" array: the results are not immediately available, and what you are doing is just composing array operations. `computeP` forces the computation to happen _in parallel_. Since GHC is very capable of optimizing intermediate data structures away, this code can compile to very efficient machine code -- there is no mutability at all, yet you get very safe and efficient parallelism.PS: what Haskell is really bad at so far is safe in-place updates for arbitrary data types (like custom ADTs), which is solved by Rust cleanly. GHC 9.0 added Linear Types [5] to address this problem. Haskell and Rust are both bad at safe in-place updates of arrays, vectors, etc., and are likely to be bad for a long time -- their indices are arbitrary integers, which cannot be analyzed statically at compile time. Haskell probably can solve this problem completely when it gets dependent types [6], whereas Rust is unlikely to support DT any time soon, if ever [7].
[1] https://hackage.haskell.org/package/stm-2.5.0.1/docs/Control...
[2] https://hackage.haskell.org/package/parallel-3.2.2.0/docs/Co...
[3] https://hackage.haskell.org/package/async-2.2.4/docs/Control...
[4] https://hackage.haskell.org/package/repa
[5] https://github.com/ghc-proposals/ghc-proposals/blob/master/p...
[6] https://github.com/ghc-proposals/ghc-proposals/blob/master/p...
Btw. Who likes C++ templates? They are such a roundabout way to program the compiler that it regularly hurts me. You need to understand a set of arbitrary rules (steered by implementation of the existing compilers, not language design, it seems) to make the compiler do what you want in many lines of code. And only fellow template lovers will have a chance to understand your intention. It is admittedly powerful, but compared to any decent macro implementation (Lisp, Haskell, OCaml, Rust) it is so much worse...
0. https://github.com/ThePhD/sol2 1. https://github.com/ThePhD/sol2/blob/develop/include/sol/func...
Saying I like them is a stretch, but I prefer to write a C++ template rather than a macro (C++ or Rust) any day.
I don't see why decidability should render them incomparable.
Exactly, they are macros that can only be invoked via some weird implicit entry points. Proper macros could be used very much in the same way, of course, if the compiler recognizes their output correspondingly. You could, for instance, use macros to generate traits/type classes to enhance overloading. Why someone would prefer this code generation to be so implicit, I don't understand.
> I don't see why decidability should render them incomparable.
Of course you can compare them if you want to point out that particular difference. But if you claim that templates are more powerful, the obvious answer is: Sure, because they're not type safe. Templates simply aren't a type system extension, they're more like macros.
You might want to consider a setup where the library author is not the same person as the library user. Don't you think it's important that the former sees and not the latter? That's why linker errors and compiler errors and template errors are not equal. Also, if you make an error in a template or its instantiation you might not even get an error but an infinite (i.e., maybe aborted) computation. If you don't see the difference between these things, I cannot help you.
Could you elaborate in which way C++ templates are not type-safe? Does the upcoming concepts feature alleviate any of it?
C++ Templates are not type safe because they need to be evaluated before you can make any statement about their successful, correct application. This evaluation might not terminate. Macros habe the same issue, of course, but they explicitly give you a Turing-complete programming language so it feels more natural.
They work very well and are quite pleasant to work with.
I don't really see what's so complicated about them.
If you have proper macros you don't need to know the compiler internals. You have an API that generates code with the language semantics you already know. By definition that's simpler. It's also more powerful, btw.
What exactly is your perceived problem? You need to have the template definition in order to instantiate it; that is pretty logical. Is this kind of basic common sense what you call "knowing the inner workings of the compiler"?
The first step prior to overload resolution is name lookup, which discards template candidates whose declaration refers to dependent names on template parameters which wouldn't exist for that implicit instantiation.
The only order that matters is what specializations are visible at the time of the instantiation, and adding more specializations after that is more likely than not to lead to ill-formed programs (due to the ODR rule that you must satisfy), so you can assume it doesn't happen.
enable_if, recursion or whatever else you're referring to are merely trivial applications of the rules.
I don't think it's unreasonable to have to understand how overloads are selected in order to program in C++. The semantics were not designed to match an implementation but rather to make the most sense. This has nothing to do with compiler internals; if anything many compilers implemented it incorrectly for decades.
Most large C++ codebases that have been running in production over decades are unfortunately a Frankenstein's monster written in a mix of old style C with classes C++ code full of malloc and free footguns, heavily OOP driven prefer-inheritance over composition era code full of virtual destructor footguns, and the modern nicer RAII std::unique/shared_ptr memory management based C++ code. And a lot of template and macro magic thrown in across each of those eras.
So you can get really productive in modern C++ pretty quickly and it can be an enjoyable experience. But to maintain a large old C++ codebase you are going to have to grok a lot of stuff. So much that I really can't find too many people who can hold it all in their head. I certainly can't.
Finally, am not sure if there was a hint of derision about Rust being primarily used for web services but I think it's a great thing if the next generation of web services will be developed in something as efficient as Rust rather than more resource hungry counterparts like Python or JS. Good for the balance sheet. Good for the environment. Good for the web devs who get expressive easy to understand code with a lot of guard rails built in.
> what you save on the computer center gets spent in developer workstations.
um, that cat is already out of the bag. unless you work on mobile or web frontend most backends are big, ugly, have a lot of dependencies, running them needs ALL the RAM anyway :)
Building from scratch Gentoo style not so much.
I seldom compile third party dependencies.
I think it's the whole x-rewritten-in-rust-now-faster hype train. Although those limitations you mention, what's easy and hard to do, do have a more or less automatic impact on performance, fueling that train.
I think of it like this:
Programmer level 1: Makes something work in a higher level language
Programmer level 2: Makes something work in a lower level language
Programmer level 3: Makes something fast in a lower level language (in most cases just a few simple optimisations on top of #2)
Programmer level 4: Makes something fast in a higher level language (object pooling, understanding how the GC and VM works to avoid GC pauses at the wrong time and all that)
So with Rust (or yes, C++, though it's a bit more of a foot gun IMHO), level 2/3 people can write code that's efficient. What I think is somewhat unique about Rust is that it keeps programmers from making lots of common mistakes working with lower level languages - like it's designed for level 1/2 folks.
Then there's the plethora of other advantages you get from its great type system and inference etc.
I think some C/C++ devs take a perverse pride in their language not being suitable to write web servers, because webdev is not real programming anyway (excepting the 'systemsy' parts like nginx).
Well, Rust is great at systems, great at web servers, pretty good at one-off script, even pretty usable for frontend. Every time I hear someone pigeonhole it as just a systems language, I think - you're missing out.
The error messages in particular - so very expressive.
That's what Rust actually is useful for. And what C++ should have been used for. It has grown in the abomination it is, because the people (well, yes, me too ;) abused it as language for 'everything'. I do actually fear that the same will happen to Rust.
Btw. most people use Rust to write web services because most people actually write web services ;)
Have you tried Zig? Comptime templating it one of its selling points.
As far as C replacements go, Zig has my vote for the most approachable but also the most well thought-out. I couldn't sing its praises enough.
std::mutex m;
void good()
{
std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
f(); // if f() throws an exception, the mutex is released
if(!everything_ok()) return; // early return, the mutex is released
} // if good() returns normally, the mutex is released var m: std.Thread.Mutex = .{};
fn good() !void {
m.lock();
defer m.unlock();
try f();
if (!everything_ok()) return;
}
It's still the RAII pattern. The explicit call to deinitialization does not disqualify it. I can overcomplicate it with a LockGuard class too: const LockGuard = struct {
mutex: *std.Thread.Mutex,
fn init(mutex: *std.Thread.Mutex) !LockGuard {
mutex.lock();
return .{
.mutex = mutex,
};
}
fn deinit(lg: *LockGuard) void {
lg.mutex.unlock();
}
};
fn good() !void {
var lk = try LockGuard.init(&m);
defer lk.deinit();
try f();
if (!everything_ok()) return;
}
In this case it demonstrates an improvement over C++, which is what happens if an error happens during a constructor. In C++ throwing an exception in a constructor is essentially UB; in zig it's a regular function call just like anything else.https://en.wikipedia.org/wiki/Resource_acquisition_is_initia...
Regarding errors in a C++ constructor: https://isocpp.org/wiki/faq/exceptions#ctors-can-throw
I am curious, what are some actual things you have done with templates that you couldn't do with generics? I'm pretty firmly in the camp of "most of C++'s strengths over C are bad". So I'm struggling.
I'd maybe possibly say C++ templates are better for math? Which is true. Rust generics are not great for complex math. However eigen is nightmare monster that absolutely ruins my compile times so I'm not sure whether this counts as a win or loss for templates. Maybe a bit of both.
Rust is still duper bad at UI. Even if there are some imgui-like Rust libraries doing cool things. I've never seen a C++ GUI library that didn't suck horribly. But at least it was possible.
Sure you can do everything you'd want to do in C++ in Rust, but the developer experience when actually writing code is agonizing most of the time. I deeply miss C++ templates in most other languages, excepting fully dynamic languages, and D. Rust is, to me, more of a replacement for places I'd write C, and be in a C mindset. C++ (well-written, modern C++) is so much more than C, and so much more than people give it credit for. A true C++ contender would have to pass not just the C bar but the C++ bar as well, rather than looking at C++'s strengths over C and saying "actually those are bad things" like it's been popular to do for the last 10 years.
Speaking of replacing C, Rust is not without its strengths: the one place I find Rust truly a joy to write is OS dev, and similar low-level projects, in which the concepts and limitations of the language map to quite nicely to the expectations of the system. It seems like the obvious candidate for these types of applications (including embedded development) but there's little surrounding literature - it always baffles me that most people seem to be using Rust to write ... web services?