7 ms·
I’m learning rust right now and there is a lot to like. Steady updates like this are also very motivating. The ecosystem feels very sane - especially compared t
by sfvisser 6y ago
I’m learning rust right now and there is a lot to like. Steady updates like this are also very motivating. The ecosystem feels very sane - especially compared to npm. Top notch Wasm support, cross compiling is a breeze.
That said, coming from a FP background (mostly Haskell/JS, now TS) Rust is... hard. I do understand the basic rules of the borrow checker, I do conceptually understand lifetimes, but actually using them is tricky.
Especially in a combinator world with lots of higher order functions/closures it’s often completely unclear who should own what. It often feels my library/dsl code needs to make ownerships decisions that actually depend on the usage.
Anyways, I guess this gets easier over time, right? Should I avoid using closures all over the place? Should my code look more like C and less like Haskell?
[edit] great answers all, providing useful context, thanks
- ragnese 6y ago> Anyways, I guess this gets easier over time, right? Yes. > Should I avoid using closures all over the place? Not necessarily. > Should my code look more like C and less like Haskell? Yes. Others sometimes don't like to hear this, but IMO, Rust is not at all functional. Passing functions around is not ergonomic (how many function types does Rust have again? Three?). Even making heavy use of Traits, especially generic ones, is difficult. Rust is very much procedural. Java-style OOP doesn't work because of the borrowing/ownership. And FP style function composition doesn't work without Boxing everything. But then you'd need to be careful about reference cycles.
- vmchale 6y ago> how many function types does Rust have again? Three? It has to, right? ATS has many function types as well, plus stack-allocated closures (I think Rust has that too??)
- steveklabnik 6y agoRust's closures do not heap allocate unless you box them, like any other struct, because closures are sugar for a struct + a function, that's correct. (and yes, there are three types of closures, because they need to know if they take said struct by reference, by mutable reference, or by owner.)
- ragnese 6y agoIt does have to, because of the way mutation and ownership work. Which is great! But it makes functional programming awkward. The language does not "steward" you toward function composition.
- estebank 6y ago> how many function types does Rust have again? Three?). Depending on what you meant, there are more than three: * There are 3 traits, used by closures depending on their needs: * Fn(Args) -> Output * FnMut(Args) -> Output * FnOnce(Args) -> Output * *Every* `fn` is its own type (`fn() {foo}`) * Function pointers (`fn()`), which is how you pass the above around in practice > Rust is very much procedural. I think this is like saying Python is very much procedural: true, but loses some nuance. Rust has some attributes of OOP, some attributes of FP. Some constructs from OOP and FP are made harder once you involve borrowing. Saying it is procedural conjures images of Pascal and K&R C in people's minds. To bolster your argument, though, I mostly use method chaining for iterators but every now and then I need to turn it into a `for` loop to keep the lifetimes understandable for the compiler, myself and others.
- ragnese 6y agoI realized after I wrote the comment that I was really referring to the closure traits when I said that. And I really should have said "kind" instead of "type" because, like you said, every different function has its own type. But anyway, I don't really disagree with your point about categorizing languages as OOP, Procedural, or Functional. But honestly, in this case, I think it's pretty damn clear than Rust is procedural WAY more than it's either OOP or FP. (Note: By OOP, I mean Java-style with tall ownership hierarchies and object-managed mutable state, not necessarily caring about inheritance. And definitely not referring to Alan-Kay-Style-OOP a la Lisp and Smalltalk). Scala can be looked at as FP and/or OOP. C++ can be looked at as Proc and/or OOP. Python, IIRC, can kind of do all of them, but I don't remember it being easy to make copies/clones in Python, so FP is questionable. Have you ever tried to write two versions of a complex async function in Rust? One with async and one with Futures combinators? Due to ownership, the Futures combinators approach very quickly devolves into a nightmare. The language doesn't "want" you to do that. What about function composition? Very awkward to do with matching up the different Fn traits. And deeply nested object hierarchies are a no-go, too, because of the inability to do "partial borrows" of just a single field of a struct. I mean, yes, it's not C because it has a real type system and generics. But... it's pretty much C in that you just write functions and procedures that operate on structs. EDIT: Perhaps my "hardline" approach on calling Rust procedural is in response to people who have come to Rust from non-FP languages, see `map`, `filter`, and `Option` and start calling Rust functional. That's not functional programming! Ask someone who does OCaml to try out Rust and see if they call Rust functional afterwards.
- int_19h 6y agoI think that Rust is often assumed to be functional because it has ADTs and nice pattern matching, both of which have historically been a feature specific to FP. Just goes to show how fuzzy our definition of FP really is...
- ragnese 6y agoAgreed. Same with "OOP". Who the hell knows what people really mean when they say that. Those people who think that "FP" means "type system like Haskell" are wrong, though, IMO. It precludes languages that are much more function-based, such as Clojure, Schemes, Elixir.
- wtetzner 6y agoFunctional in my mind more or less means means working with immutable values.
- scns 6y agoUsually when the term OOP is used, it actually means ClassOrientedProgramming (C++/Java/C# etc)
- ragnese 6y agoWhich I also don't understand. Is that a style of overusing classes where functions would suffice (FooHelper)? Or is it something about the language? Because almost all popular languages have classes. Rust and Go call them "struct", but it's the same thing. Swift has "class" and "struct", but they're both the same thing as a C++ class.
- iopq 6y agoRust structs are not classes. Rust puts structs inline (on the stack), classes are virtual. Since Rust is a systems programming language, it's an actual distinction that makes a difference
- 6y ago
- vmchale 6y ago> I do understand the basic rules of the borrow checker It ends up being doable. I dabbled in ATS, developed Stockholm syndrome, and now Rust ain't too bad. Higher-order functions are difficult in Rust or with linear/affine types in general. Haven't looked at what Rust does recently. > Should I avoid using closures all over the place? Should my code look more like C and less like Haskell? When in Rome do as the Romans :) Anyway, some fun imperative programming stuff you can do in Rust that is fickle in Haskell (or OCaml/Standard ML).
- thenewwazoo 6y agoI don't have time for an exhaustive answer, so I'll give you some rules of thumb when using Functional-style combinators: * If you need to keep unchanged the input, you must either use a reference-to (.iter()) or copy-of (.iter().cloned()) of each item * If you don't need the input ever again, you should move the items (.into_iter()) These rules follow for each step of the chain. I very very often write very Functional code in Rust and I find it natural and easier to reason about than imperative-style code. The example I could find the fastest: https://github.com/thenewwazoo/aoc2019/blob/master/src/day10.rs#L66 https://github.com/thenewwazoo/aoc2019/blob/master/src/day10... Edit: another example (this one uses types that are Copy so the copies are implicit) https://github.com/thenewwazoo/cryptopals/blob/master/src/transform.rs#L65 https://github.com/thenewwazoo/cryptopals/blob/master/src/tr... Another edit: I am not a Functional programmer, and have never known Haskell or any Lisp. Erlang is as close as I've ever gotten. I've found Rust to be a fantastic language for writing Functionally.
- trumpeta 6y agoI've been using .to_owned() liberally, that often does the trick in the first instance, albeit at the potential cost of a copy.
- estebank 6y agoThis is a perfectly reasonable solution. You might be leaving performance on the table but 1) if perfomance isn't a measurable problem for you, then there's on point on eking the last bit of performance from these allocations 2) it simplifies the code itself 3) sometimes clones are actually efficient, people forget to make their small ADTs Copy 4) if you're learning the language this lets you delay the moment when you have to fully understand the way lifetimes actually behave in complex cases, which means that when you do do that you will have a better grasp of the rest of the language and will be able to form a better mental model of how it fits with other features
- stevencorona 6y agoThis echoes my experience with learning Rust over the past few weeks (coming from Elixir). There is a lot to like, understand lifetimes conceptually, but it's hard.
- dnautics 6y agoIf you're coming from elixir, and not doing this for work, I highly suggest zig; zig feels like elixir since both have the comptime concept.
- nemothekid 6y ago>the borrow checker, I do conceptually understand lifetimes, but actually using them is tricky. I've been using Rust for a little over year, almost daily at work, and for several projects. I have a pretty good intuition about how the borrow checker works and what needs to be done to appease it. That said, I don't think I'm any closer to understanding lifetimes. I know conceptually how they are supposed to work (I need the reference to X to last as long as Y), but anytime I think I have a situation that could be made better with lifetimes, I can't seem to get the compiler to understand what I'm trying to do. On top of that very little of my code, and the code I read actually uses lifetimes.
- dahfizz 6y agoIt's worth noting that every reference in Rust has a lifetime, but the compiler is usually smart enough to infer it. What you are talking about is explicit lifetimes.
- adsjhdashkj 6y agoAs someone in a similar description as you - i find my lifetime understanding... moderate. Complex lifetime usage still can tweak my brain - notably how i can design it. But simple lifetime usage is intuitive. A simple example i often run into is wanting to do something with a string, without taking owned parts of the string. Very intuitive how the str matches the lifetime of the owned value. On the otherhand, the other day i was trying to write a piece of software where: 1. I wanted to deserialize a large tree of JSON nodes. I had the potential to deserialize these nodes without owning the data - since Serde supports lifetimes, i could deserialize strings as strs and hypothetically not allocate a lot of strings. 2. In doing that, because a tree could be infinitely large i couldn't keep all of the nodes together. Nodes could be kept as references, but eventually would need to be GC'd to prevent infinite memory. 3. To do this, i _think_ lifetimes would have to be separate between GC'd instances. Within a GC'd instance, you could keep all the read bytes, and deserialize with refs to those bytes. When a GC took place, you'd convert the remainder partial nodes to owned values (some allocation) to consume the lifetime and restart the process with the owned node as the start of the next GC lifetime. ... or so my plan was. I have, i think, just enough understanding of lifetimes to _almost_ make that work. I _think_ some allocations would be required due to the GC behavior, but it would still reduce ~90% of allocations in the algorithm. Unfortunately, i got tired of designing this complex API and just wrote a simple allocation version. Conceptualizing allocations and the lifetimes to make it work are.. interesting. Especially when there is some data within the lifetime that you want to "break out of" the lifetime, as in my example (where i had a partial node, and i made it owned). I still think i understand enough to do it - it'll just take a fair bit of thinking and working through the problem.
- zozbot234 6y ago> The ecosystem feels very sane [] compared to npm Now that's damning with faint praise.
- deleted 6y ago[deleted]
- Shorel 6y agoYep. I made my first Rust script last week and the amount of care required is similar to C++. It is definitely not easier compared to C++, contrasting with D, which is easier than C++. However, the program worked correctly at the first try, which I guess it is also a consequence of the Rust model.
- Fiahil 6y agoAh, if you’re making dsl code or functional combinators, you usually want to ‘move’ your values instead of ‘borrowing’ them. example: fn add(mut self) -> Self { self } fn add(self) -> Self { self } instead of: fn add(&mut self) {} fn add(&self) {} With this, you will be able to ‘store’ closures easily and apply them later. No more fighting with the borrow checker over where to borrow as mut or not. You will also avoid a few copies.
- stjohnswarts 6y agoSometimes you will be annoyed by changes I guarantee it BUT since 1.0 that's decreased a lot and compared to npm it's night and day. You'll think you're dealing with C in relative terms of stability if npm is your baseline :D