News for hackers. Stuff that matters.
Re:Rust vs. Haskell by amelius on Tuesday February 14, 2023 @08:10AM (#34788763)
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.

Re:Rust vs. Haskell by kzrdude on Tuesday February 14, 2023 @08:41AM (#34789082)
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();
Re:Rust vs. Haskell by ghostwriter on Tuesday February 14, 2023 @08:59AM (#34789314)
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.
Re:Rust vs. Haskell by kzrdude on Tuesday February 14, 2023 @10:29AM (#34790687)
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;)

Re:Rust vs. Haskell by solomonb on Tuesday February 14, 2023 @01:42PM (#34793968)
I think they meant trivial in comparison to Haskell's type inference, which you had started off comparing against, not C++.
Re:Rust vs. Haskell by ghostwriter on Tuesday February 14, 2023 @02:39PM (#34794780)
> 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 << ' '; }
    }
Re:Rust vs. Haskell by majewsky on Tuesday February 14, 2023 @05:31PM (#34797158)
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).
Re:Rust vs. Haskell by ghostwriter on Tuesday February 14, 2023 @07:15PM (#34798211)
Sorry, I still don't get how it's different from C++, and my point wasn't about `auto` but rather about `std::vector v2`:

    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 type

The 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...

[3] https://www.cppstories.com/2021/concepts-intro/

Re:Rust vs. Haskell by pjmlp on Tuesday February 14, 2023 @08:37AM (#34789025)
Also Haskell is getting linear types, which gets us the best of both worlds, GC productivity and low level performance when needed.
Re:Rust vs. Haskell by platz on Tuesday February 14, 2023 @09:19AM (#34789631)
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.
Re:Rust vs. Haskell by pjmlp on Tuesday February 14, 2023 @09:29AM (#34789776)
They are on the early days of integrating them, so how could we expect them to refactor GHC all the way already?
Re:Rust vs. Haskell by ghostwriter on Tuesday February 14, 2023 @09:25AM (#34789702)
What would be an example of significant elision in this context?
Re:Rust vs. Haskell by anon291 on Tuesday February 14, 2023 @09:45AM (#34790002)
> 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.

Re:Rust vs. Haskell by zozbot234 on Tuesday February 14, 2023 @08:36AM (#34789010)
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.
Re:Rust vs. Haskell by amelius on Tuesday February 14, 2023 @09:07AM (#34789429)
> 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.

Re:Rust vs. Haskell by zozbot234 on Tuesday February 14, 2023 @09:38AM (#34789910)
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.
Re:Rust vs. Haskell by marcosdumay on Tuesday February 14, 2023 @09:10AM (#34789473)
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.)

Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @09:31AM (#34789812)
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.

Re:Rust vs. Haskell by chongli on Tuesday February 14, 2023 @09:42AM (#34789976)
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.
Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @09:47AM (#34790045)
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.

Re:Rust vs. Haskell by chongli on Tuesday February 14, 2023 @10:03AM (#34790271)
Wait, they got rid of proc closures? Hahaha. Shows how long it’s been since I last looked at Rust. Okay, now they do look quite natural!
Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @10:20AM (#34790538)
Indeed. My (and by no means am I the only one) mental model of closures is really simple. A closure is a struct with no name, and the free variables of the closure are fields in the struct. If the closure is annotated with 'move', then those fields are owned, otherwise the fields are borrowed and the lifetime parameter on the struct is the shortest of the bunch. Then, calling the closure is just calling it with 'self' (FnOnce), '&self' (Fn) or '&mut self' (FnMut).

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));
    }
Re:Rust vs. Haskell by chongli on Tuesday February 14, 2023 @10:30AM (#34790698)
That’s really nice. One of my favourite uses for returning closures is to separate a function into ‘configuration’ and ‘execution’ stages. It becomes extremely useful when you have a ton of parameters but only a few change between typical invocations.
Re:Rust vs. Haskell by Hirrolot on Tuesday February 14, 2023 @10:42AM (#34790908)
Your example indeed perfectly demonstrates the subtlety of closures in Rust. Now let's imagine I want to return a nested closure, e.g., `impl Fn(i32) -> impl Fn(i32) -> i32`, or something like that:

    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.
Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @10:51AM (#34791073)
My example didn't demonstrate the subtlety here, but it inspired your example, which of course does demonstrate some subtlety. And as I acknowledged in another comment, I of course agree there are subtleties to closures in Rust! And I agree there are useful nightly features that unlock additional use cases. Rust gives you enough rope to hang yourself with. For example, if you're okay with a heap allocation (like I assume you might be in a language with "non-broken closures" lol), then you don't need nightly Rust:

    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.
Re:Rust vs. Haskell by Hirrolot on Tuesday February 14, 2023 @09:56AM (#34790177)
Closures in Rust are fundamentally broken. See [1] for the discussion.

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

Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @10:14AM (#34790437)
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!

Re:Rust vs. Haskell by Hirrolot on Tuesday February 14, 2023 @10:39AM (#34790860)
> "fundamentally broken" and yet I've been using them with little to no friction for pretty close to a decade now.

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!

Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @10:46AM (#34790983)
The sneer is strong with this one.

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

Re:Rust vs. Haskell by pitaj on Tuesday February 14, 2023 @11:16AM (#34791534)
That's not just closures, but a confluence of closures, async, and more.
Re:Rust vs. Haskell by Hirrolot on Tuesday February 14, 2023 @12:02PM (#34792324)
The example provided in the post -- yes. But I can basically demonstrate other language features that are not working with closures, such as generics -- you just can't have generic closures in the same way as you have generic functions, even in a synchronous code. With closures, you have a pretty limited scope of what you can do, considering the rest of the language, so I think it'd be pretty useless to consider closures as a separate mechanism that shouldn't interact with the rest of the language.
Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @12:08PM (#34792425)
> But I can basically demonstrate other language features that are not working with closures, such as generics -- you just can't have generic closures in the same way as you have generic functions, even in a synchronous code.

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."
Re:Rust vs. Haskell by Hirrolot on Tuesday February 14, 2023 @12:37PM (#34792898)
Well, your example is not a generic closure, since inside `adder`, you already have a generic type `T`; it's introduced by the `fn` keyword. If we try to do the same for a closure, we would face a type error. This issue has been discussed on SO [1].

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

Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @01:11PM (#34793441)
> Well, your example is not a generic closure, since inside `adder`, you already have a generic type `T`; it's introduced by the `fn` keyword. If we try to do the same for a closure, we would face a type error. This issue has been discussed on SO [1].

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

Re:Rust vs. Haskell by SnowflakeOnIce on Tuesday February 14, 2023 @11:25AM (#34791681)
I understand your point and perhaps I'm in fierce agreement.

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

Re:Rust vs. Haskell by burntsushi on Tuesday February 14, 2023 @11:29AM (#34791754)
Well yes... For many reasons. Including automatic currying and the fact that eta reductions work seamlessly in Haskell and less so in Rust.
Re:Rust vs. Haskell by debuggerpk on Tuesday February 14, 2023 @10:01AM (#34790243)
this comment made my day.
Re:Rust vs. Haskell by weavie on Tuesday February 14, 2023 @09:44AM (#34789994)
Infix operators and currying give Haskell completely different ergonomics to Rust, even if you could largely do the same thing in both languages.