News for hackers. Stuff that matters.
Re:The Path to Rust by Animats on Thursday May 26, 2016 @01:10AM (#11775629)
Well, the functional crowd won. An example expression from the parent article:

    let idx = args
    // iterate over our arguments
    .iter()
    // open each file
    .map(|fname| (fname.as_str(), fs::File::open(fname.as_str())))
    // check for errors
    .map(|(fname, f)| {
      f.and_then(|f| Ok((fname, f)))
        .expect(&format!("input file {} could not be opened", fname))
    })
    // make a buffered reader
    .map(|(fname, f)| (fname, io::BufReader::new(f)))
    // for each file
    .flat_map(|(f, file)| {
      file
        // read the lines
        .lines()
        // split into words
        .flat_map(|line| {
          line.unwrap().split_whitespace()
            .map(|w| w.to_string()).collect::<Vec<_>>().into_iter()
        })
      // prune duplicates
      .collect::<HashSet<_>>()
        .into_iter()
        // and emit inverted index entry
        .map(move |word| (word, f))
    })
  .fold(HashMap::new(), |mut idx, (word, f)| {
    // absorb all entries into a vector of file names per word
    idx.entry(word)
      .or_insert(Vec::new())
      .push(f);
Is there editor support for indenting this stuff?
Re:The Path to Rust by lifthrasiir on Thursday May 26, 2016 @01:57AM (#11775860)
More readable, and IMHO more idiomatic version: (Disclaimer: never tested)

    let mut idx = HashMap::new();
    for fname in &args {
      let f = match fs::File::open(fname) {
        Ok(f) => f,
        Err(e) => panic!("input file {} could not be opened: {}", fname, e),
      };
      let f = io::BufReader::new(f);
      let mut words = HashSet::new();
      for line in f.lines() {
        for w in line.unwrap().split_whitespace() {
          if words.insert(w.to_string()) { // new word seen
            idx.entry(w.to_string()).or_insert(Vec::new()).push(fname);
          }
        }
      }
    }
People who was introduced to the functional approach for the first time seems to enjoy it so much that everything becomes a hard-to-read mess of functions. I had similar experiences with Python list comprehension and C# LINQ.
Re:The Path to Rust by lmm on Thursday May 26, 2016 @09:26AM (#11777702)
You can have it all though - at least, you can in Scala, Rust ought to support something similar. Something like (hybrid syntax/pseudocode):

    (for {
      fname <- args
      f = fs::File::open(fname).orElse(
        e => panic!("input file {} could not be opened: {}", fname, e))
      r = io::BufReader::new(f)
      w <- f.lines().flatMap {
          line => line.unwrap().split_whitespace()
        } .distinct
    } yield Map(fname -> Vec(w.toString))).sum
Just as concise as yours (more concise even), but easier to reason about and safer to refactor. Possibly you might need to do the mutable version for performance if this was a hot loop, but you don't get to even ask that question until you've profiled it and found out that it was the bottleneck; 99.9% of the time it isn't.
Re:The Path to Rust by bjz_ on Thursday May 26, 2016 @02:18AM (#11775948)
That one looks to be more efficient too - all those `collect`s in the original are a tad concerning...
Re:The Path to Rust by Animats on Thursday May 26, 2016 @02:10PM (#11780202)
Do the "collects" mean that memory is allocated and the whole collection stored temporarily, or is this all iterative generators?

(I thought I knew Rust, but I haven't used it for a year, and its usage has changed a lot even if the core language hasn't.)

Re:The Path to Rust by steveklabnik on Thursday May 26, 2016 @02:46PM (#11780469)
collect will ... collect it all into some kind of data structure, so yeah, that version would be doing a bunch of allocation.
Re:The Path to Rust by k_bx on Thursday May 26, 2016 @08:00AM (#11777232)
In my taste, it's also more correct to write "for" in a code which is more like statements (not expressions), and only use map/fold for stuff which is pure.
Re:The Path to Rust by jupp0r on Thursday May 26, 2016 @03:57AM (#11776364)
Sorry, but I disagree. Although more verbose (as in more characters of source code), I could easily understand what the original code did. Your code has a high cyclomatic complexity and I have to keep all those nested for loops in mind when trying to figure out what you are doing.

I write C++ for most of my day job, and this would not pass code review because of readability problems in my team.

Re:The Path to Rust by gpderetta on Thursday May 26, 2016 @05:05AM (#11776599)
I like to use (even abuse) lazy iterators in C++ as the next guy, but IMHO the imperative version in this case is not only shorter but significantly more readable.

The two loops contain a single statement and it is trivial to see what's going on. It is also easier to extend. And it is still using lazy iterators (I assume) where it makes sense, i.e. in the split_whitespace call.

Re:The Path to Rust by alex_muscar on Thursday May 26, 2016 @06:58AM (#11777001)
I cannot upvote you enough.
Re:The Path to Rust by lifthrasiir on Thursday May 26, 2016 @04:08AM (#11776413)
I know there are obviously lots of rooms for improvements. Indeed, if the code would become more complex I would immediately refactor. I can list some problems with my example:

- The error handling should really have been refactored. `try!` would make this easier. (I haven't used it since it is in the `main` function and the code was a direct replacement.)

- Error during reading words is not accurately handled. Again, `try!` would make this easier.

- `words` and `idx` look coupled to each other, which ideally shouldn't have been.

- `words.insert` is quite an opaque method; not everyone is sure if it returns true on a duplicate key or not. I've added a comment but frankly I'm not satisfied of that. <deleted> One alternative is to use `words.entry` instead, which gives a named enum variant. </deleted> Oops, `HashSet` does not have `entry`...

- Ultimately, the body of the outermost loop should go to a function.

That said, do you really think that `line.unwrap().split_whitespace().map(|w| w.to_string()).collect::<Vec<_>>().into_iter()` is a chunk of code which can be read at a glance? It at least has to be named (like T-R's example). Functional approach means that you can split functions and individually review them; the original code, IMHO, didn't.

Re:The Path to Rust by T-R on Thursday May 26, 2016 @04:35AM (#11776498)
I think the issue with procedural loops (not to be critical, just in general) is that there's no abstraction - it's not clear what's getting mutated, or what the result is (or its type), it's harder to look at the individual steps, and it's harder to refactor.

The nice thing about functions like "map", "filter", "fold", "flatmap", etc., is that they describe intent - when you see a "map", you know it's not aggregating things, just applying a function: "map" has a distinct purpose from "fold" (and depending on how pure things are, you may not even be allowed to do anything crazy).

Aside from that, the higher-order functions have pretty intuitive algebraic laws for refactoring (like with "compose" mentioned in my other comment) - It's not clear how you'd factor code out of a nested loop without understanding the whole thing, whereas "flatmap" (a.k.a. "bind" for the list monad) has laws for refactoring it.

Re:The Path to Rust by lifthrasiir on Thursday May 26, 2016 @05:02AM (#11776589)
I agree to you with the general sentiment. That said, Rust is not a functional language per se; the degree of functional decomposition is thus fundamentally limited. This is partly because it tries to be efficient, and as arielb1 pointed out, ownership sometimes makes it worse.

> It's not clear how you'd factor code out of a nested loop without understanding the whole thing, [...]

You are right, loops are particularly hard to refactor. I still argue that my code is better (barring any future expansion) because it fits within handful lines; you cannot easily understand 100 lines of code with the cyclomatic complexity of 1, but you can often easily understand 10 lines of code with the cyclomatic complexity of 8 (e.g. triple loops). The size of code, syntax and contextual information matters as much as algebraic laws.

Re:The Path to Rust by arielb1 on Thursday May 26, 2016 @04:27AM (#11776468)
That part is certainly extreme because of ownership. It can be refactored to

        let mut words = HashSet::new();
        for line in file.lines() {
            let line_words = line.unwrap().split_whitespace();
            words.extend(
                line_words.map(|s| s.to_string())
            );
        }
        words.into_iter().map(move |word| (word, f))
Re:The Path to Rust by kzrdude on Thursday May 26, 2016 @04:37AM (#11776500)
The "idomatic" version (I agree with that) simply works better. You can insert the typical `try!()` calls that short-circuit on error and return errors to the caller.

Retrofitting better error handling in the single expression version requires changing the shape of the whole thing, editing the expression throughout.

Re:The Path to Rust by ngrilly on Thursday May 26, 2016 @03:06AM (#11776161)
Definitely more readable to my eyes.
Re:The Path to Rust by wott on Thursday May 26, 2016 @04:53AM (#11776557)
Yes, it is both much shorter and immediately readable and understandable (without comments!), unlike all the Functional Programming style versions which were proposed in the comments.
Re:The Path to Rust by T-R on Thursday May 26, 2016 @03:03PM (#11780611)
This is maybe true, given that Rust is intentionally multi-paradigm, but it's a pretty unfair comparison: My FP-style version deliberately made almost no changes to the original code aside from adding names - I even stated in the comment that I don't even know Rust (haven't touched it beyond knowing what "Affine types" means, and once having gotten someone else's "hello world" to compile for ARM). The imperative version you're comparing it to completely changes the semantics of the program, and is written by someone very familiar with the language.
Re:The Path to Rust by ngrilly on Thursday May 26, 2016 @08:48AM (#11777483)
Yes. Functional programming is really useful and powerful in some situations, but sometimes the old school imperative programming is what you need. There is a reason why all cooking recipes are written in an imperative style.
Re:The Path to Rust by agumonkey on Wednesday June 01, 2016 @03:39PM (#11817478)
The upper map-full code is also badly formatted and has comments everywhere. It's true that chaining functional abstractions is tempting at first; but with a bit of balance:

    let words = fn (f, file) => {
            file.lines().flat_map( |line| { 
                line.unwrap()
                    .split_whitespace()
                    .map(|w| w.to_string())
                    .collect::<Vec<_>>()
                    .into_iter()
            })
            .collect::<HashSet<_>>()    // prune duplicates
            .into_iter()
            .map(move |word| (word, f)) // and emit inverted index entry
        }

    let idx = args.iter()
        .map(|fname     | (fname.as_str(), fs::File::open(fname.as_str()))) //+ (str, fd)
        .map(|(fname, f)| { f.and_then(|f| Ok((fname, f))).expect(&format!("input file {} could not be opened", fname)) })
        .map(|(fname, f)| (fname, io::BufReader::new(f)))                   //+ (str, buf)
        .flat_map(words)                                                    //+ {str}
        // absorb all entries into a vector of file names per word
        .fold(HashMap::new(), |mut idx, (word, f)| { idx
                                                     .entry(word)
                                                     .or_insert(Vec::new())
                                                     .push(f);
        });                                                                 //+ {word:[filename]}
Re:The Path to Rust by Jonhoo on Thursday May 26, 2016 @11:29AM (#11778671)
I think it'd actually be good to show both to show how versatile Rust can be in this regard. I added it to the article with a link back here. Thanks!
Re:The Path to Rust by T-R on Thursday May 26, 2016 @02:49AM (#11776095)
Please, please, please don't associate inlining everything with functional programming. Lambda-lift and name those functions. If you name them, you can ditch most of those comments, too (by moving that information into the names). Not too familiar with Rust syntax, but (to rewrite without changing semantics) something like:

    let open_file = |fname| (fname.as_str(), fs::File::open(fname.as_str()) );

    let check_err = |(fname, f)| {
      f.and_then(|f| Ok((fname, f)))
        .expect(&format!("input file {} could not be opened", fname)) };

    let to_buff_reader = |(fname, f)| (fname, io::BufReader::new(f));

    let line_to_words = |line| {
      line.unwrap()
          .split_whitespace()
          .map(|w| w.to_string())
          .collect::<Vec<_>>().into_iter() };

    let get_file_words = |(f, file)| {
      file.lines()
          .flat_map(line_to_words)
          .collect::<HashSet<_>>().into_iter() // prune duplicates
          .map(move |word| (word, f)) };       // emit inverted index entry

    let add_to_index = |mut idx, (word, f)| {
      idx.entry(word).or_insert(Vec::new()).push(f);
      idx };

    let idx = args.iter()
      .map( compose(to_buff_reader, check_err, open_file) ) //[1]
      .flat_map(get_file_words)
      .fold(HashMap::new(), add_to_index);
[1] Does Rust have a compose function? I hope so. If not, you may need 3 successive "map"s. And stream fusion.
Re:The Path to Rust by lifthrasiir on Thursday May 26, 2016 @03:27AM (#11776239)
> Does Rust have a compose function? I hope so.

Rust itself does not have a compose function. But a closure works fine, so `|fname| to_buff_reader(check_err(open_file(fname)))` would work. If you want to go further, a nightly Rust allows for this kind of construction:

    #![feature(unboxed_closures, fn_traits)] // that's why we cannot use stable (yet)
    
    struct Composed<F, G>(pub F, pub G);
    
    impl<F, G, T, U, V> FnOnce<T> for Composed<F, G>
            where F: FnOnce<T, Output=U>, G: FnOnce<(U,), Output=V> {
        type Output = V;
        extern "rust-call" fn call_once(self, args: T) -> V { self.1(self.0.call_once(args)) }
    }
    
    impl<F, G, T, U, V> FnMut<T> for Composed<F, G>
            where F: FnMut<T, Output=U>, G: FnMut<(U,), Output=V> {
        extern "rust-call" fn call_mut(&mut self, args: T) -> V { self.1(self.0.call_mut(args)) }
    }
    
    impl<F, G, T, U, V> Fn<T> for Composed<F, G>
            where F: Fn<T, Output=U>, G: Fn<(U,), Output=V> {
        extern "rust-call" fn call(&self, args: T) -> V { self.1(self.0.call(args)) }
    }
    
    fn main() {
        println!("{}", Composed(|x| x+3, |y| y*4)(5)); // 32
    }
Re:The Path to Rust by T-R on Thursday May 26, 2016 @03:45AM (#11776307)
> a closure works fine, so `|fname| to_buff_reader(check_err(open_file(fname)))` would work

Ah, yes it would. Clearly I'm a bit too tired to be writing code, if I've overlooked function application. I suppose it doesn't have to be point-free. =)

Pretty cool, though - thanks for the info.

Re:The Path to Rust by yoklov on Thursday May 26, 2016 @09:54AM (#11777930)
Rust is on my to-learn list (and I'll likely have to learn it for work anyway) but dear god what a horror. That is firmly in the same echelon as C++ template madness.
Re:The Path to Rust by lifthrasiir on Thursday May 26, 2016 @10:17AM (#11778100)
While you can do lots of horrible things with generics (cough typenum [1] cough), it has a rather strict rule that prevents it from ever having the same degree of freedom as C++ template has. In particular, this entire thing is correctly type-checked and you won't see hacks like SFINAE. Probably things like this should be encapsulated in a separate crate, reviewed and maintained by the community.

[1] http://paholg.com/typenum/typenum/index.html

Re:The Path to Rust by tatterdemalion on Thursday May 26, 2016 @01:25PM (#11779791)
I guarantee you its a far cry from what templates will let you do.

However, its overloading a custom type to behave like a higher order function, which is a fairly complex piece of code. Most uses of generics are much simpler.

Re:The Path to Rust by discreteevent on Thursday May 26, 2016 @04:26AM (#11776463)
You are correct to point this out (not to associate inlining everything with FP). However a lot of functional code seems to use this style. Maybe people need to point this out more frequently and raise it in code reviews. To me it's like really bad academic English. The kind that uses the passive voice a lot and uses phrases like "the former" and "the latter", instead of naming things and using short clear sentences. Sometimes I'm convinced that the author of the code, if they were honest, would admit that they had difficulty keeping track of exactly what they were referring to while they were writing it.
Re:The Path to Rust by T-R on Thursday May 26, 2016 @05:07AM (#11776608)
Oh, it definitely pops up a fair bit, but it really is just not-well-factored code. It's a direct parallel to having long boolean expressions, or long equations without breaking out any sub-expressions and storing them in named variables - after all, functions are just sub-expressions - which is something Code Complete specifically advocated against in procedural/OOP code.

Some people seem to fall back into it a bit when they discover point-free style (and that seems to make up a lot of what you see in mixed-paradigm code). I don't think it'd be controversial to say it's bad practice in any paradigm, so the association of it with FP is kind of like judging web programming by late 90's beginner PHP code (which, at one point in time, did describe a lot of web programming, but it was never good, and we'd like to put that behind us).

Re:The Path to Rust by kzrdude on Thursday May 26, 2016 @04:39AM (#11776510)
Closure type inference doesn't work well enough in Rust, so your refactoring will not compile unfortunately. Rust needs the closures to be used inline.
Re:The Path to Rust by bigger_cheese on Thursday May 26, 2016 @01:36AM (#11775753)
The syntax doesn't look all that friendly. Even with comments on every second line of the rust code I struggled to work our what it was doing.

I opened the external example he linked which has solutions in a number of languages. (https://www.rosettacode.org/wiki/Inverted_index) I could understand the C version of the code pretty well at a glance. Granted it was long but it looks like the intuitive way I'd write the code. I acknowledge my bias here.

I looked at some other languages on the site and both the the C# example and D example look much more readable then the rust code (and I've not used either of those languages either I think they are intended for "system" programming as well) the code as provided in those languages seemed more intuitive.

Re:The Path to Rust by Jonhoo on Thursday May 26, 2016 @01:41AM (#11775782)
To some extent, I think this comes down to familiarity with the functional approach to solving these kinds of problems. Certainly, before I started writing code in Rust, I also preferred the imperative approach you would take in C and Go. Over time though, I've found that I increasingly prefer this way of expressing chained computation.

That said, there's certainly a point to be made (and indeed other commenters have made it already) that this isn't code you would want to maintain. For production-level code, it'd be broken down more, so that individual pieces could be reviewed and tested in isolation. The code example here was to demonstrate the expressivity of the language more so than the One True Way of doing it.

Re:The Path to Rust by danieldk on Thursday May 26, 2016 @02:03AM (#11775883)
Over time though, I've found that I increasingly prefer this way of expressing chained computation.

This looks like code from the 'I found out about FP, let's apply it everywhere'-stage. For instance, if the iteration over arguments was done with a regular for...in loop, the filename would be visible in its scope and the rest of the code could be simplified due to not having to pass the filename everywhere.

Also, I don't think this demonstrates expressivity well, because the purported functional construct uses unwrap/expect (it panics). The expressivity of Rust allows you to have the whole expression to have Result has its type fairly easily.

(Sorry for ranting a bit, but I think that Not only is this very readable, will tick off people. Which is sad, because it could be changed into something which is more readable but still expressive.)

Re:The Path to Rust by Jonhoo on Thursday May 26, 2016 @02:11AM (#11775929)
I agree with your point in that this is written in an excessively functional style. Something more akin to the imperative-style solution given by lifthrasiir below might have been simpler. However, I don't think that code shows that Rust is more expressive in any way than C/Go is (which is kind of the point).

The functional code I give in the article is quite radically different from what you would/could do in those languages, and I would argue that it does show that Rust provides some interesting and flexible mechanisms that add to the expressivity you have as a programmer. Sure, it's overused in the example, but the exaggeration at least has a chance of communicating the point. Maybe the argument is that I should have used a simpler (yet still not trivial) example instead, but I couldn't think of one at the time.

Re the "Not only is this very readable" part, I completely agree. I could change it to something less, hmm, bold, but I'm not sure it would make that much of a difference at this point. Suggestions are welcome.

Re:The Path to Rust by pjmlp on Thursday May 26, 2016 @03:48AM (#11776323)
I remember reading about a nice post from someone in the Clojure community that de constructed an almost unreadable program full of nested function declarations into something that any of us would be happy doing maintenance.

It went thought the code identifying code patterns and goals, and then making them into separate functions or data structures as appropriate.

Re:The Path to Rust by pcwalton on Thursday May 26, 2016 @01:54AM (#11775844)
I noticed that a lot of the Rust programs on Rosetta Code use the functional style as well. You could do that in C# and D too if you wanted—it's just that the people adding Rust entries to the site prefer functional style.

Rust does not force you into the functional style, and usually I use imperative style when writing Rust code, except when the task is so simple that writing e.g. a for loop would be just noise. For example, if I just wanted to convert every string in a vector to uppercase, I'd probably just write "list.iter(|string| string.to_uppercase()).collect()". But if I were, say, hashing every string in the vector, I'd probably use a for loop over an iterator.

Re:The Path to Rust by pjmlp on Thursday May 26, 2016 @03:44AM (#11776306)
If I would be using Rust I think I would also be in the FP crowd.

There are enough similarities with ML languages, and I have used so much FP concepts all the way back to Caml Light, my first FP language, that I would just naturally follow that path when writing Rust.

In Smalltalk it was just natural to mix both styles.

Re:The Path to Rust by ngrilly on Thursday May 26, 2016 @03:11AM (#11776184)
> Rust does not force you into the functional style, and usually I use imperative style when writing Rust code, except when the task is so simple that writing e.g. a for loop would be just noise.

I use the same strategy in "dynamic" languages like Python and JavaScript, that support both an imperative and functional style.

Re:The Path to Rust by smitherfield on Thursday May 26, 2016 @01:59AM (#11775870)
A lot of stuff in the article's code examples struck me as "hacky," although I have no idea what's considered idiomatic/performant Rust.
Re:The Path to Rust by EugeneOZ on Thursday May 26, 2016 @02:10AM (#11775920)
It's just question of time, I promise.
Re:The Path to Rust by pcwalton on Thursday May 26, 2016 @01:32AM (#11775735)
I wouldn't say "the functional crowd won"—that's certainly not how I would write the code, and it's not the style that high-profile projects like Servo typically use. Rather, the author clearly prefers functional style, and Rust (like most modern languages) allows you to write in that style if you wish.
Re:The Path to Rust by Jonhoo on Thursday May 26, 2016 @01:37AM (#11775761)
Of course. This example was given to show expressiveness. For production-level code, you would break it up more so pieces can be tested individually (as pointed out in another comment).
Re:The Path to Rust by devin_lane on Thursday May 26, 2016 @01:35AM (#11775752)
I dunno, I wouldn't say this is as readable as the author mentions. To me, all of the chaining, syntax for closures, and Rust concepts like "into_iter" work to hide the actual algorithm, which is pretty simple. And forget testing, since this is all actually ONE top level call. I certainly wouldn't want to maintain code written like this in any language (and yeah, while you could make pretty much the above in C++ if you really wanted, I wouldn't write it that way.)
Re:The Path to Rust by danieldk on Thursday May 26, 2016 @02:39AM (#11776064)
How I would've written it. Rust beginner, but 'been there, done that' in FP:

https://gist.github.com/danieldk/3bd3b84c1a7c8bc8c902314a488...

(Compiles and seems to work properly on a small document collection.)

Re:The Path to Rust by Chris2048 on Thursday May 26, 2016 @07:40AM (#11777156)
> // iterate over our arguments

> .iter()

hmmm. Not far off the old

> i++ // add one to i

Re:The Path to Rust by tatterdemalion on Thursday May 26, 2016 @01:29PM (#11779829)
These are quite different. `.iter()` is equivalent to creating the counter and declaring both the increment and conclusion condition, only its guaranteed to correctly access each member of the collection once and to never be broken, and for many collections (e.g. arrays) it is written in a way which allows for powerful optimizations.
Re:The Path to Rust by Chris2048 on Friday May 27, 2016 @05:16AM (#11784808)
I mean in terms of comment useful-ness.
Re:The Path to Rust by dikaiosune on Thursday May 26, 2016 @01:17AM (#11775666)
Rustfmt can clean that up, but I'm on mobile and can't run the examples through to see what it looks like.
Re:The Path to Rust by Jonhoo on Thursday May 26, 2016 @01:31AM (#11775729)
If you compare that to many of the other solutions on Rosetta Code (https://www.rosettacode.org/wiki/Inverted_index), that's a pretty neat solution! And yes, as pointed out below, rustfmt would clean this up (mostly by increasing the indentation). I reduced indent to double spaces to make the code denser for the post.
Re:The Path to Rust by conradev on Thursday May 26, 2016 @04:59AM (#11776581)
Rustfmt generally handles this well, yes.