Hacker News new | ask | show | jobs
by armchairhacker 1221 days ago
This article paints Rust and Haskell as very similar languages, but when I saw the title I was thinking about how they are fundamentally different. Rust is technical and exposes/requires you to understand the reality of computation, while Haskell hides it away and lets you program in a system based on lambda-calculus (you don't even have strict order of evaluation!) Interesting to see that they are very similar regardless.
6 comments

Quite a few of Rust's features were borrowed from or inspired by ones in Haskell - albeit often modified to be more suitable for systems programming (and systems programmers!).

Most Haskellers I know are quite fond of Rust :)

I am (or used to be) a Haskeller, and I just can’t get over how spectacularly ugly Rust’s syntax looks compared to Haskell or SML and finally force myself to learn it. Call me shallow, but it’s true.
I love how Haskell splits the type definition and function definition into two lines. It’s so readable that way. Also enables outlining your program first with just type definitions, and then going back and filling in the function definitions later.
No, you're not shallow. Rust's syntax has put me off a lot. I was looking to learn an alternate Erlang VM language and liked Gleam, which originally had a Haskell/ML syntax, but they've decided it's a popularity contest, so they chose to go to an Algol, C, Rust looking syntax. Shame. I was learning LFE (Lisp Flavoured Erlang), buecause Lisp, but then Elixir took off because all of the Ruby programmers took to it (and it's got a lot of other stuff going for it, but it's closer to Ruby than LFE!).
I share this opinion. Haskell was my hobby language for 2 years until Rust. I still miss the brevity of Haskell and point-free style in particular.
I think Rust's features were actually inspired by OCaml rather than Haskell. Rust was originally written in OCaml, and OCaml feels a lot more similar to Rust than Haskell does.

Of course Haskell is heavily influenced by ML so it also has a lot of the same features.

Traits are definitely inspired by Haskell typeclasses (according to Graydon), but you are right that many other similarities are also shared with ML languages.
> I think Rust's features were actually inspired by OCaml rather than 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 :)

There does exist at least one sml implementation that computes the memory allocations at compile time a bit similar to how Rust does it.
It doesn't have to be one or the other :). OCaml and Haskell are two great languages that the Rust designers were familiar with.

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.

IMO, that's too superficial to compare them. So what if both have a bunch of similar looking constructs? Rust is imperative, and sequential, and nearly everything is mutable, hence the borrow checker. That makes programming in Rust rather different.
Haskell controls mutable data by pervasive immutability.

Rust controls mutable data by the borrow checker system: allow sharing OR mutation but not both at the same time.

I get what GP is implying. The way you program (and think) is entirely different if you are working with immutable data structures and functions versus safe mutability.
Rust is immutable by default. You have to deliberately opt in for mutability. When you don't opt in to mutability the programming styles are very similar at a basic level.

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.

Rust's new claim is now that references isn't immutable, it's shared by default. There is no concept of immutable data in rust. All data is mutable in various ways and that is a product of the binding not the data, from the cell interior mutability eupphamisn to shadowing in the same frame: (`let x = 2; let x += y;` is valid even though x isn't defined as mutable. The mental gymnastics a rust parishioner has to go thrown to shout down other languages while theirs fills with the same concepts is growing by the hour.
You can build "product types with recursive values" in any language that remotely descends from C or Algol. Pattern matching is not wide spread, but --while nice to have-- it's syntactic sugar.

> When you don't opt in to mutability

Rust's whole shtick is safe mutability.

>You can build "product types with recursive values" in any language that remotely descends from C or Algol.

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.

In most Haskell codebases most code is written imperatively and executes sequentially.

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.

I think painting Rust and Haskell as similar languages is very misleading.

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.

Rust has closures and does not use a GC. The borrow checker just makes sure that a closure never outlives its captures or any of its references. The most prominent difference between Rust and Haskell is that Haskell uses lazy evaluation throughout; it's the language's unique selling point. Rust doesn't even have generators in the stable variety of the language yet, although they are internally used to implement async/await.
There is nothing "natural" about working with Rust closures. If you want to take a losing fight against the borrow checker, they are the best way to do it.

(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.)

Closures work very well in Rust and are a core aspect of the standard library. The 'Iterator' trait adapters, for example, make very heavy use of closures, as do the combinators on 'Option' and 'Result'. Closures are also how you spawn threads. They are absolutely ubiquitous and quite natural.

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 think the word ‘natural’ in this context is too vague. If you’re coming to Rust from C++, sure, Rust’s closures feel quite natural. If, on the other hand, you’re coming to Rust from Haskell then they’ll feel wholly unnatural and confusing.
Sure it's vague. We're expressing opinions here. :-) But it's important to push back here. Closures are absolutely natural enough in Rust that a huge swath of very fundamental APIs in Rust rely on them. If they weren't natural in at least some sense, it would be absolutely bonkers to do that.

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.

I understand your point and perhaps I'm in fierce agreement.

OTOH, things like parser combinators are much more ergonomic in Haskell than Rust.

Well yes... For many reasons. Including automatic currying and the fact that eta reductions work seamlessly in Haskell and less so in Rust.
Closures in Rust are fundamentally broken. See [1] for the discussion.

[1] https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-...

Lol. "fundamentally broken" and yet I've been using them with little to no friction for pretty close to a decade now. If that's what you think "broken" means, then, well, you've completely devalued the word. This right here is what we call sensationalism folks.

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!

That's not just closures, but a confluence of closures, async, and more.
this comment made my day.
> The borrow checker just makes sure that a closure never outlives its captures or any of its references.

The point is that the borrow checker forces you to program in a fundamentally different style than idiomatic Haskell.

You are not forced into any design choice. You can use explicit reference counting to enable closure captures to add to object lifetime, or even a lightweight tracing GC implementation (such as the recently developed Samsara https://redvice.org/2023/samsara-garbage-collector/ ) to collect cycles as Haskell does. Of course, Rust makes the performance tradeoffs involved quite clear, even though the implementations it can use are quite possibly leaner than Haskell's.
Infix operators and currying give Haskell completely different ergonomics to Rust, even if you could largely do the same thing in both languages.
You should look at what you can do with the Rust type inference too, it's not too far away from Haskell, at least not superficially. For example using the return type to deduce type parameters for a method like let samples: Vec<_> = iterator.collect();
But this is the most trivial example of inference. Rust has neither generic implementation of higher kinded data nor global inference for the existing specific cases of it like GAT.
Is it the most trivial? C++ doesn't support it, and it has type inference using `auto`.

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;)

> For example using the return type to deduce type parameters for a method like let samples: Vec<_> = iterator.collect();

> 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 << ' '; }
    }
C++ auto can only do forward type-inference, i.e. the RHS must have a definitive type and this type is what `it` will end up having in your example. In the Rust example from above:

  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).
I think they meant trivial in comparison to Haskell's type inference, which you had started off comparing against, not C++.
Also Haskell is getting linear types, which gets us the best of both worlds, GC productivity and low level performance when needed.
Every talk I've seen on Haskell linear types is that at best they can do some specific compuatations more efficiently (especially with arrays) but are not a good fit in Haskell to significantly elide GC.
> but are not a good fit in Haskell to significantly elide GC

That's because the entire haskell ecosystem would have to be rewritten which seems insurmountable.

They are on the early days of integrating them, so how could we expect them to refactor GHC all the way already?
What would be an example of significant elision in this context?
Rust is the closest thing to Haskell that I can run on a processor without an MMU.

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.

They are extremely similar.

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

I see Rust as heavily influenced by ML, perhaps even descended from it loosely, just with C family syntax. This seems to be a commonly expressed view? Rust has everything ML has, pretty much. Rust's concept of ownership/RAII was first implemented in an experimental ML variant, one that didn't need garbage collection. (The first version of Rust was implemented in Ocaml, perhaps not coincidentally.) And of course, ML and Haskell are close cousins, if not siblings.
> you don't even have strict order of evaluation

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)