Dhghomon / rust-fsharp

Rust - F# - Rust reference
MIT License
239 stars 14 forks source link

Pipelining section question #2

Open isaacabraham opened 3 years ago

isaacabraham commented 3 years ago

I'm not sure (warning: I'm not a Rust dev so might be completely wrong here) that the two examples (map.filter vs addOne |> addOne) are really equivalent.

Isn't the map. filter etc. nature of that sample more about library functions, iterators and how collections are evaluated? In F#, probably the closest would be using Seq e.g.

let answers =
    [ 1 .. 10 ]
    |> Seq.map (fun x -> x * 2)
    |> Seq.filter (fun x -> x > 5)

And indeed in the example above, no real computation happens until you "evaluate" the query using e.g. Seq.toList or some other function that executes the sequence.

So, yes, |> does eagerly evaluate, it's really just syntactic sugar for calling the last argument in a function on the LHS of the pipeline - but that doesn't preclude the kind of lazy evaluation you're referring to.

Might be completely wrong here though, so feel free to ignore this if I've misunderstood!

Dhghomon commented 3 years ago

That does look a lot closer, yes, since you're just setting up a non-eagerly evaluated structure that doesn't actually do anything until you try to do something with it. I think it's different on the back end since here it's making a seq whereas the Rust-ish version of the F# type might be like a filter<map<seq>>, but I guess sequences themselves are lazy so maybe the end effect is similar. In the meantime this will be good to add to the section (since Rust users will want to know about sequences for sure).

ChayimFriedman2 commented 3 years ago

In the same manner, this part is wrong:

This is because calling these iterator methods without collecting them or assigning them to a variable just makes a big complex type; we haven't mapped or filtered anything yet. Let's see what it looks like by getting the compiler mad:

fn main() {
    let times_two_then_even: i32 = (0..=10) // Tell the compiler it's an i32
        .map(|number| number * 2)
        .filter(|number| number % 2 == 0);
}

The compiler complains:

expected type `i32`
     found struct `Filter<Map<RangeInclusive<{integer}>, 
     [closure@src/main.rs:3:14: 3:33]>, [closure@src/main.rs:4:17: 4:41]>`

So all we've done is put together a type Filter<Map<RangeInclusive etc. etc. etc. And if we call .map a whole bunch of times it just keeps on putting this big struct together:

found struct `Map<Map<Map<Map<Map<Map<Map<Map<RangeInclusive<{integer}>, [closur
e@src/main.rs:3:14: 3:33]>, [closure@src/main.rs:4:14: 4:33]>, [closure@src/main
.rs:5:14: 5:33]>, [closure@src/main.rs:6:14: 6:33]>, [closure@src/main.rs:7:14: 
7:33]>, [closure@src/main.rs:8:14: 8:33]>, [closure@src/main.rs:9:14: 9:33]>, [c
losure@src/main.rs:10:14: 10:33]>`

This struct is all ready to go, and gets run once when we actually decide to do something with it (like collect it into a Vec). The pipeline operator in F#, however, gets run every time you use it: if you do something like this:

let addOne x = x + 1;

let number  = 
    addOne 9
    |> addOne
    |> addOne
    |> addOne
    |> addOne
    |> addOne
    |> addOne

It's going to call addOne every time, and same for F#'s iterator methods.

The pipe operator in F# exactly equals the calling syntax (a |> b is exactly the same as b (a), just more convenient sometimes). And same in Rust, a.b() equals b(a) (or b(&a), or b(*a), or b(&mut *a), but that is because of deref coercion). The difference between Rust and F# in this example is that Rust iterators are lazily evaluated, and F# List methods are not. Still, as mentioned bt @isaacabraham, F# has Seqs which are lazy.