4 ms·
Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuff
by nyanpasu64 4y ago
Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call.
Separately, to express code which creates multiple non-const references into a single object (trivial in any other imperative or OOP language), you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell and raw pointers, unsafe confusing (and currently unsound, see https://github.com/rust-lang/rust/issues/63818 https://github.com/rust-lang/rust/issues/63818) Pin, or experimental ghost cells (https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf https://plv.mpi-sws.org/rustbelt/ghostcell/paper.pdf, not sure the current implementations).
- hansonkd 4y agoYMMV but I personally have never faced either problem having written tens of thousands of lines of Rust at this point. But to be honest I never worked on a large team of Rust engineers. One of the benefits of Rust is that it gives you an option to switch to imperative style if thats what you choose which isn't the case for Haskell or purely functional languages. Tacit programming was never a problem as the IDE does a pretty phenomenal job of telling you the types anyway. If the IDE can't figure out the type, the compiler won't either. non-const references in a single object can simply be solved by making the whole object mutable, which is essentially what Python et al. do for you anyways. I never had to use any unsafe code or pointers. Not sure if that way of thinking is carried over from C and C++ engineers. The only time I think I use Cell or the like is if I have a global static variable that I need to fill in at runtime. But that is more of a code-smell for most or at best a more advanced programming design that beginners don't need to worry about. The best way to solve the last problem is to understand your data model from the start and make what objects need to be mutable mutable.
- jononor 4y agoTo be fair, this kind of thing even happens in JavaScript. Some use functional programming with immutable datastructures, higher order functions, super generic code, using some niche FP library with its own idiosyncrasies - when simple and imperative would do just great.
- cogman10 4y ago> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call. More and more, I find this is just a general problem and not a rust specific one. Any language with enough syntax flexibility will have a group of programmers that try to treat the language like lisp or haskell. (I recently ran into exactly this with a typescript lib I've been working with). C++ is no stranger to this problem, it's famously the reason Java doesn't have operator overloads. What I'm also finding is that complex style of programming is annoying to find in the company code base, but is rarely one that catches on in the ecosystem as a whole.
- epage 4y ago> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type. My preference is usually to write imperative code matching over objects or looping over iterators, except in cases like "minimum of list" where the loss of clarity of a loop exceeds the loss of clarity of a reduce() method call. I know for myself, I overdid it at times as I was learning the trade offs. Where I've currently settled is simple map/filter operations to tweak things into a more readable state are better than the more procedural approach. Also keep to simpler combinators (e.g. no `map_or` / `map_or_else`). If things become too complicated, be procedural. If there are side effects, be procedural. Another way I have for framing it is the code layout (files, functions, blank lines, newlines, indentation) should be used to highlight the business logic. Early returns, `?`, post-fix combinators, etc should be used to reduce boilerplate that detracts from the business logic.
- nextaccountic 4y ago> If there are side effects, be procedural. Yep! That's why Iterator::for_each https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html#method.for_each https://doc.rust-lang.org/stable/std/iter/trait.Iterator.htm... is more often than not a code smell [0]; it exists mostly for the sake of writing side effects in functional style. So something like something.map(|x| ..).for_each(|x| do_something(x)); Should probably be written let iter = something.map(|x| ..); for x in iter { do_something(x); } It's still okay to use Iterator::map and other iterator combinators, but using for instead of for_each here is probably easier to read. [0] One exception is in the documentation "In some cases for_each may also be faster than a loop, because it will use internal iteration on adapters like Chain."
- nextaccountic 4y ago> Some Rust programmers, when faced with performing operations on a sum type (like Result) or container, tend to reach for point-free map-filter-reduce code stuffed with higher-order functions, lambdas, trait generics, and return type inference, requiring looking up the documentation or implementation of multiple helper microfunctions, and asking the compiler for the inferred return type Can you give an example of that? Do you mean something like, using .map() and .map_err() on Result? I can't imagine myself not using those methods; they make programming much more enjoyable, and should be considered elementary as of 2023. (Note: about .map_err() specifically, error conversion is kind of important in Rust; it's usually performed automatically by the question mark operator but sometimes you need to do it explicitly, and doing so without .map_err() usually obscures the intent of the program for no gain in simplicity.)
- AnIrishDuck 4y ago> Separately, to express code which creates multiple non-const references into a single object (trivial in any other imperative or OOP language), So multiple mutable references to the same data? I'm personally happy that's harder to do in Rust, as it enables anti-patterns like God Objects [1] and spooky action at a distance [2]. I don't like the "you're doing it wrong" response when people point out flaws, and that's not what I'm trying to say here. You can write perfectly fine code with remote mutation. This is just one example where the things that Rust chooses to make harder are also the things I have a harder time understanding. I'll also agree that remote mutation is still necessary for some important use cases (e.g. testing, memoization). Then you have the `Cell` / `RefCell` escape hatches. Rust is pragmatic about such things. > you need compromises like verbose Cell, overhead RefCell, ugly and footgun-filled UnsafeCell ... The actual need for these constructs tends to be rare in my experience. We have tens of thousands of lines of Rust in our codebase and ~five instances of these wrappers. They're really not hard to avoid. And actually, looking through the results here, I've found a few cases we actually _shouldn't_ be using `RefCell`. > unsafe confusing (and currently unsound ...) Pin This is a fair point. `Pin` always felt to me like a compromise to get async out the door. The best thing I can say is it's proof that the Rust team is willing to not let the perfect stand in the way of the useful. But I'll concede it still is difficult to understand and complicates the async picture. This is definitely an example where Rust's underlying philosophy makes things that should be easy harder. 1. https://en.wikipedia.org/wiki/God_object https://en.wikipedia.org/wiki/God_object 2. https://en.wikipedia.org/wiki/Action_at_a_distance_(computer_programming) https://en.wikipedia.org/wiki/Action_at_a_distance_(computer...