Soft question: functional programming in rust feels 'clunky'


#1
  1. I have previously used C/C++/Clojure/Haskell.

  2. When learning Rust, I fully adopted the imperative style / for-loops.

  3. I’ve ben playing alot with “functional style” in Rust lately mostly collections -> iterators -> filter/map w/ closure

  4. I don’t know if it’s lack of IDE support or if I am doing something wrong, but trying to do “functional programming” in Rust feels very clunky compared to both (4a) imperative Rust and (4b) functional programming in Clojure/Haskell.

  5. Anyone else feel this way? (If not, maybe I’m not skilled yet.)


#2

What do you mean by clunky? Is it perhaps there isn’t formatting or a nice common syntax between languages, or something else?


#3

I’m not sure, I have not identified the precise problem (thus the soft question). but I’ve noticed it takes me much longer / I am much more error prone writing Rust/iterator/functional style code compared to Rust/imperative code.

Also, I stand why the syntax is often .map( | ... | { ...; ....; ... }) but it still feels unnatural / end up being error prone for any non-trivial closures.

I am almost at the point where I define the closure before hand and apss it to map, i.e.

let my_closure = | ... | { .... ; ... ; ... }
foobar.map(my_closure)...

This is just one thing, I’m sure there are other elements at play too, but I have not identified them besides “I am very error prone coding this way.”


#4

It might be:

  1. Different functional combinators have different input and output args. Sometimes it’s references, sometimes it’s values and sometimes it’s references to references. Most lambdas don’t have type ascription so you end up needing to learning the stuff to the point where you can mentally map them.
  2. Occasionally, the functional methods don’t allow you to return a reference from the input arg as an output.
  3. You need to know capture semantics well for closures.

I think a lot of it is just familiarity so give yourself more time.


#5

To add to @vitalyd:

  1. The complexity of many chained iterators replacing a for loop
  2. The mutation of values along an iterator’s steps
  3. Typing for functions and their parameters for custom values
  4. Lifetimes for complicated generic Fn* trait signatures
  5. The explicitness of .collect on an iterator and the fact that iterators are lazy

#6

I feel the same way. I decided Rust is an imperative language, and do not try to write anything in functional style these days.


#7

I’ve had the opposite experience of @sanxiyn. More often than not, the functional style is more readable to me than the corresponding for-loop. One benefit is fewer mutable temporary variables. Another advantage is when the iterator can return a Result or an Option. You need a match in the for-loop but not in functional style.


#8

If your map usage often contains a { …; …; … } that could be your problem.


#9

Also, I stand why the syntax is often .map( | ... | { ...; ....; ... }) but it still feels unnatural / end up being error prone for any non-trivial closures.

What’s so error-prone about that syntax?


#10

I find myself somewhere in the middle between @sanxiyn and @alanhkarp. I do a lot of processing of long iterator chains for analyzing vectors of atmospheric data. I use itertools a lot. I’ve found in cases where I have for loops that have a lot of steps, the functional style is easier to read and separates the processing into logical chunks. In cases with a simple reduction (or fold for functional programmers) a for loop can be clearer. Sometimes I build an iterator in the functional style and then reduce it with a for loop.


#11

The alternative would be simple one-line exprs for map/filter. In Clojure/Haskell, writing multi-line map/filters has never been a problem.


#12

This often ends up being something like

blahblahblah
.map(|blahblah| {
   ...;
  ...;
   foo.bar() } ) // sometimes I get this line wrong due to editing
// and it's hard to see that it's the source of the error
.blahblah

EDIT 1:
In contrats, if everything was () like in Clojure/paredit, this problem goes away.

EDIT 2:
I know that most IDEs will auto insert balanced () {}'s, but sometimes my editing gets them unbalanced.


#13

To me by far the biggest trouble with functional programming in rust is the ownership semantics. Seldom do I find that I can just .map(existing_fn) because oftentimes I need to take a reference to the values, and this is something you can’t do with an iterator adapter.

fold is even less likely to have a function with a compatible signature.


Re: syntax, I always format it like

blahblahblah
    .map(|blahblah| {
        ...;
        ...;
        foo.bar()
    })
    .blahblah

#14

This often ends up being something like

blahblahblah
.map(|blahblah| {
   ...;
  ...;
   foo.bar() } ) // sometimes I get this line wrong due to editing
// and it's hard to see that it's the source of the error
.blahblah

While Typescript was growing over the last five years and IDE’s were slowly maturing support. Sometime types from chained functions won’t propagate properly, or an error won’t show correctly.

Yet, with last chaining in your example, if an error changes type of a thing before .blahblah, while blahblah is expected by you, compiler can’t highlight the reason for mismatch with your expectation. Same effect will be in already mature Typescript tools.
I’ll go as far as to suggest that any language will have this, cause computer doesn’t read your thoughts, while we are using nice implicit typing. You may add some explicit expected typing with variables, but then cleanliness of chaining with .'s will be gone. The functional cleanliness has some cost to it :wink:

May be we should write off this particular setting to watch out for, when figuring out errors.


#15

Re: syntax, I always format it like

Thou shall not impose one’s own indentation in non-python places :smile:
Thou may use linting tools in one’s own projects/work :smile:


#16

This is interesting. I’ve noticed that one point of frustration is that:

  1. IntelliJ/Rust rarely freezes when I’m writing code outside of closures.

  2. IntelliJ/Rust often “freezes” (possibly calculating types) when I am editing inside a Closure.


#17

This is interesting. I’ve noticed that one point of frustration is that:

  1. IntelliJ/Rust rarely freezes when I’m writing code outside of closures.
  2. IntelliJ/Rust often “freezes” (possibly calculating types) when I am editing inside a Closure.

Typescript tools used not to pick proper typing in .-chains. Now they do. It seems to be a question of tools maturity. Is it RLS (Rust Language Service) that fails? I don’t have an answer.


#18

In my opinion Rust needs some kind of the Haskell do-notation or F# computation expressions (the latter is applicable both for creating monadic computations and generators like sequences, lists, arrays and so on). It is quite possible to do, if we apply FnOnce and if we generate computations anew each time we need the computation like that how Future does from the futures-rs crate. The Future trait can be considered a monad from some perspective, where there is the corresponding monadic bind known as the and_then combinator.

I do a very similar thing in my own code manually using monads and continuations too. It quite works. But the resulting code indeed looks awkward.

I hope that such a syntactical sugar will be added to the Rust language sometime. I hoped that it will be done for the Future computation, but it seems to me that introducing the async/await syntax will delay this happy event for some indefinite time.


#19

Have you tried setting up rustfmt to automatically format your code in your editor? It’s absolutely awesome for stuff like this. Plus, you can use it as a very quick check that your syntax is valid and avoid wasting time compiling code that is wrong.


#20

When I did functional style code, the biggest pain point was always getting all the &s, *s, and refs in the right places and amounts in the lambdas. It seems that lambdas are just implicit enough to make things confusing and annoying. And code that works outside of a lambda often doesn’t work inside a lambda and vice versa, due to subtle differences in the coercion rules and type inference.