Most Haskellers I know are quite fond of Rust :)
Rust controls mutable data by the borrow checker system: allow sharing OR mutation but not both at the same time.
First the type system is very similar with the capability of building sum and product types with recursive values. Second pattern matching over sum types is also very very similar.
Rust is like the bastard child of C++ and haskell.
> When you don't opt in to mutability
Rust's whole shtick is safe mutability.
You cut off the part where I said sum AND product types. Variant and union are post C++11 and not traditionally part of Algol style languages. Even nowadays the only time I see variant is from some haskell engineer stuck on a C++ job.
>Pattern matching is not wide spread, but --while nice to have-- it's syntactic sugar.
No pattern matching is a safety feature. Most of rust code should be using pattern matching as much as possible. In fact there's a language called Elm that achieves a claim for code that has ZERO runtime exceptions via the pattern matching feature. See: https://elm-lang.org/
Don't believe me? You know about programmings biggest mistake right? Null? Well C++ sort of got rid of it by discouraging it's use but even with optionals the issue is still there because optionals can still crash your program.
With rust, optionals never crash unless you deliberately ask it to via unwrap. If you use pattern matching exclusively to unroll optionals a crash is virtually impossible. Try it, and try to guess how pattern matching prevents optionals from crashing your program in rust, while in C++ the lack of pattern matching forces empty optionals to crash when you try to access it.
>Rust's whole shtick is safe mutability.
Yes, but at the same time it's opt in. Algol languages are by default mutable and you have to opt in for immutability via keywords like const. Rust offers extended safety via a single usage mutable borrow ans single usage mutable ownership but this is opt in. You should avoid using the keyword mut as much as possible.
It is very rare that anyone is actually using the truly advanced techniques like knot tying or TARDIS monads which are truly impossible in other languages.
Of course Haskell is heavily influenced by ML so it also has a lot of the same features.
The big thing IMO that makes Rust feel like Haskell is the pervasive use of traits and `#[derive(...)]` which is directly analogous to the pervasive use of typeclasses and `deriving` in Haskell.
The old alpha/beta rust docs specifically referenced Haskell all over the place, and I don't recall them mentioning OCaml.
> OCaml feels a lot more similar to Rust than Haskell does
I disagree very strongly with this. OCaml doesn't even have type classes/traits! Rust lacks polymorphic variants, functorial modules, etc. It's really nothing like idiomatic ocaml, whereas I think it's pretty reasonable to describe Rust as "what Haskell devs would make if they weren't allowed to heap allocate". Maybe they'd also add a few more gizmos to the type system :)
Haskell has a garbage collector for a reason.
You can't program naturally with closures unless you have a garbage collector, because closures introduce cyclic references.
Also, Haskell has a type inference system that allows e.g. the IO monad to work seamlessly with the rest of the language.
EDIT: and laziness of course.
The point is that the borrow checker forces you to program in a fundamentally different style than idiomatic Haskell.
(Rust has your C-like run of the mill function references too. People should emphasize those more, because differently from closures, those work very well.)
In contrast, function pointers are very rarely used.
Are they equivalently nice in every way to closures in Haskell? Of course not. But I think your comment is swinging way too far in the opposite direction.
I came to Rust from Haskell (among other languages). I was a little confused by closures, but I attributed that to the fact that I started writing Rust before 1.0 when closures were far far far less convenient than they are today. (This is back when there were 'proc' closures.) Now I find them extremely natural.
Everything else pretty much falls into place.
The other thing that was added since you've looked at Rust is that you can return unboxed closures because Rust has gained the ability to express anonymous types directly by naming a trait it implements:
fn adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| y + x
}
fn main() {
let add2 = adder(2);
assert_eq!(7, add2(5));
assert_eq!(10, add2(8));
} fn adder(x: i32) -> impl Fn(i32) -> impl Fn(i32) -> i32 {
move |y| move |z| y + x + z
}
Alright, I can't. I can do that for a single closure, but can't for two or more of them. Here's why we have `#![feature(impl_trait_in_fn_trait_return)]` -- in Nightly, among many other such features. fn adder(x: i32) -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> {
move |y| Box::new(move |z| y + x + z)
}
fn main() {
let add23 = adder(2)(3);
assert_eq!(10, add23(5));
assert_eq!(13, add23(8));
}
It's almost like "fundamentally broken" (not just "broken" but "fundamentally broken") and "has subtlety" are two totally different things. What a fucking revelation.[1] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...
Now if you said, "Rust has some rough points at the intersection of generics, closures and async programming," I'd say that's absolutely true!
This might rather confirm my point, since engineers using a specific programming language quite often have a contorted picture of how code in other languages is written, as they become more and more acquainted with their main tool. If we compare Rust closures with those of ML languages, it becomes pretty clear how natural closures are in ML and tricky in Rust.
Not quite a sensation either to anybody who happened to use closures in a typical Rust code, which, apparently, happens to have quite a lot of generics and async!
> This might rather confirm my point
It might, but it doesn't, because I don't only use Rust. Notice also how you've moved the goalposts from "fundamentally broken" (sensationalist drivel) to "how natural closures are in ML and tricky in Rust" (a not unreasonable opinion with lots of room to disagree on the degree of "natural" and "tricky").
Generics and closures work just fine:
use std::ops::Add;
fn adder<T: Copy + Add<Output=T> + 'static>(x: T) -> impl Fn(T) -> Box<dyn Fn(T) -> T> {
move |y| Box::new(move |z| y + x + z)
}
fn main() {
let add23 = adder(2)(3);
assert_eq!(10, add23(5));
assert_eq!(13, add23(8));
}
You keep making sensationalist generalizations. It's trivial to demonstrate that closures and generics work together just fine, as I've done above. Are there subtleties and complexities and other things that can make certain cases or areas tricky? Absolutely. But that's not the same as "not working."I haven't claimed that closures are not working, since well, they are working, but under a very limited set of circumstances. Again, I see nothing sensational, since the trickery of using closures has been discussed elsewhere.
[1] https://stackoverflow.com/questions/34814423/possible-to-def...
Of course it's generic. If you were to write the type out for the closure, it would be generic over 'T'. (Whether that type ever actually gets written out that way or not is a different matter.) As far as I can tell, what you're saying is that Rust doesn't support higher-rank polymorphism. Which is true (for types, but not for lifetimes). But that's not the same as "generics don't work with closures."
> I haven't claimed that closures are not working
You've said:
> Closures in Rust are fundamentally broken.
(which was completely unqualified and not to a "very limited set of circumstances")
and (emphasis mine)
> But I can basically demonstrate other language features that are not working with closures, such as generics
OTOH, things like parser combinators are much more ergonomic in Haskell than Rust.
That's because the entire haskell ecosystem would have to be rewritten which seems insurmountable.
Note that in my example the type information flows from the variable to picking which generic method that is called, which is reversed information flow of what I would call the most trivial case - when the variable gets its type from the method that was called. (This is trivial: let x = 1i32;)
> C++ doesn't support it
How exactly is it different?
#include <iostream>
#include <vector>
int main() {
std::vector<int> v1 = {1,2,3};
auto it = v1.begin();
std::vector v2 (it + 1, it + 2);
for (auto i: v2) { std::cout << i << ' '; }
} let samples: Vec<_> = iterator.collect();
The RHS by itself has the type "B for any B that implements FromIterator<I> where I is the Item type of the given iterator". This cannot be assigned directly to a variable without any type hint (like with C++ auto) because it's an entire class of types and not a specific type. However, in the provided example, the Rust type checker can use the provided clue to narrow this down to an exact type, Vec<I>, without requiring an explicit statement of the type I (instead the underscore serves as a placeholder). std::vector v2 (v1.begin() + 1, v1.begin() + 2);
> However, in the provided example, the Rust type checker can use the provided clue to narrow this down to an exact typeThe Rust example is incomplete and its RHS by the time it gets to `.collect()` within the context of `iterator` has to be bound to a particular type of the context via `impl Iterator for <MyContext>`. This is pretty much the same thing as template argument deduction for class templates in C++17 [1][2], and in C++20 it got extended to generic concepts and constraints [3]:
#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>
template <typename T>
requires std::integral<T> || std::floating_point<T>
constexpr auto avg(std::vector<T> const &v) {
auto sum = std::accumulate(v.begin(), v.end(), 0.0);
return sum / v.size();
}
int main() {
std::vector v { 1, 2, 3 };
std::cout << avg(v) << std::endl;
}
Note that nowhere in the snippet do I specify the type explicitly except for the container of type vector.[1] https://en.cppreference.com/w/cpp/language/template_argument...
[2] https://devblogs.microsoft.com/cppblog/how-to-use-class-temp...
There's a pareto frontier of "best available language for a given project", and I think Haskell dominates the portion of the pareto frontier where you don't have super tight physical/memory/real-time constraints, and Rust dominates the portion where you do. This is by virtue of their similarity.
Rust just happens to heavily use linear types. Haskell let's you use both. If you write your Haskell program using linear types you get borrow checker semantics.
The main difference is mem management. However haskell could likely be written to be manually memory managed. There are manual men based ocaml implementations.
Haskell is not typically used for systems programming.
> you don't even have strict order of evaluation!)
People say this but I'm not sure people understand it. Haskell evaluation order is exotic but deterministic in all cases unless you introduce threads, which bring non determinism into any language including rust
You do, with `seq`. And a lot of Haskell programming in practice is deciding between lazy vs strict programming.
The only lazy feature commonly used by Rust programmers are iterators, but they are just lazy lists (that are immediately discarded after being forced)