1. 32
  1.  

  2. 39

    And then this is my claim: The ownership model, standard library, emphasize on iterators, and numerous other things do lead to code that is usually most idiomatic when written in a functional style. Thus, the positive properties listed above that are associated with functional programming and immutability are present in Rust programs, and, to a degree, even statically enforced by the compiler. For me, that is a joy!

    I guess I don’t really agree with all of this. I agree that Rust gets the nice properties of functional languages at least when it comes to mutation, but if someone read the body of Rust code I have written and came back and said, “it really resembles the code I typically write on Lisp/ML/Haskell,” then I would be really surprised. I rarely write higher order functions. I rarely write recursive functions or folds. I rarely use persistent data structures and instead use mutation without reservation.

    I think that if I were forced to choose, I would just say that Rust is a procedural language. But of course, that leaves so much out. Such is the inherent problem with categorization in the first place.

    1. 11

      I agree with your sentiment. When I started Rust, I was using OCaml as my main programming language and I tried to emulate the OCaml style of functions taking immutable values in and returning immutable values and I quickly found myself struggling and talked about it on Reddit. I found that, although Rust does have functional programming genetic material (being bootstrapped in OCaml, GC from way-back-when that lent itself better to FP, current closure-strong collection APIs, etc.), Rust is best used as a procedural language. Trying to go against that is like writing imperative code in a functional language: it can be done, but the resulting software will be uglier, slower, and less maintainable for contributors.

      I rarely use persistent data structures and instead use mutation without reservation.

      Functional languages take away mutability from the “mutability + aliasing = bugs” equation; Rust takes away the aliasing — or at least, the uncontrolled aliasing. To me, it then makes sense to feel comfortable using mutability (and the advantages it offers), safe in the knowledge that the compiler is helping you avoid bugs.

      1. 4

        Functional languages take away mutability from the “mutability + aliasing = bugs” equation; Rust takes away the aliasing — or at least, the uncontrolled aliasing. To me, it then makes sense to feel comfortable using mutability (and the advantages it offers), safe in the knowledge that the compiler is helping you avoid bugs.

        Yes, that’s what I meant with, “I agree that Rust gets the nice properties of functional languages at least when it comes to mutation.” The OP also makes this point. My point here was to contrast typical Rust code with typical code written in a functional programming language. That is different from contrasting the goals and problems solved by the respective paradigms.

        I think we’re in agreement FWIW. Just seemed like a small misunderstanding here worth correcting.

        1. 2

          Well said! You should go with the grain of the language. Rust encourages mutation instead of superfluous copies. The borrow checker stops you from mutating in ways that are likely to cause bugs, so there’s no point in making copies when you don’t need to.

          FP is going to be like OOP in a few years. Right now everyone is trying to “FP all the things”. Monads in JavaScript? Are you kdding me? Stahp.

          I remember trying to OOP-ize everything I came across. I tried to write OO Python: a bunch of classes with “private” (double underscore) fields and side-effects, etc. It worked about as well as you’d expect.

        2. 4

          I understand your point and I tried to make clear that Rust code will unlikely equal a Haskell solution for the same problem. Not in terms of control structures and maybe not even from the overall design (Haskell maybe “world loop” deriving new state, Rust maybe event loop modifying application state directly which is probably not held in one single place).

          That said, from approaching program design in Clojure, I tend to create data structures similarly. Favoring acyclic graphs and being picky with where I change it. Rather to have some unidirectional control flow (thinking about UIs now, e.g. yew.rs, tui.rs or even gtk). In other “imperative” languages (like C++) I would approach it differently.

          As of functions: I love the versatility of iterators. Mapping or folding a sequence of results into a Result and not having to resort to for loops is nice. But I agree with higher order functions being used rarely.

          As you point out, pressing a rubber stamp on things has issues. This was meant more to provoke some thought and discussion ;)

        3. 18

          I’m increasingly coming around to the conclusion that there’s no such thing as a functional programming language; only programs that are written in a more or less functional style. A language can offer features that encourage or discourage the writing of functional programs, and when people say “language X is a functional language” what they mean is that it encourages the writing of functional programs.

          That said, any language with statements and no repl is difficult for me to describe as functional.

          1. 7

            I’ve joked before, but it’s somewhat true, that there’s really a hierarchy of “functional” which depends entirely on which other languages you sneer at for being insufficiently “functional” and which you sneer at for having uselessly gone too far in pursuit of purity.

            Like, the base level is you join the Church, you renounce von Neumann and all his works. There’s a level where you sneer at any poor pleb whose language doesn’t guarantee tail-call optimization. There’s a level where you sneer at any poor pleb whose language is based on untyped lambda calculus. There’s a level where you sneer at any poor pleb whose language doesn’t support fully monoiconic dyads bound over the closed field of a homomorphic Bourbaki trinoid. And at each level you also sneer at the people “above” you for going overboard and forgetting that programming needs to be practical, too.

            1. 4

              This is how I teach paradigms at the start of my Rust course! Programming paradigms are about a mental model for computation (“do I think of this in terms of functions / objects / logical relations / blocks / etc.”), and language support for a paradigm is about how well you can translate that mental model into code in that language. People sometimes don’t like this definition because it’s subjective, but it avoids the problems with defining paradigms extensionally or intensionally.

              If you define them extensionally, you’ll find that no one can agree on what the extent is. “Hey you left X language out of the functional list!” “Yes it isn’t functional” “But it is!” and so on.

              If you define them intensionally, you’ll find that no one can agree on what features constitute the necessary and sufficient conditions for inclusion in the set. “Functional programming requires automatic currying!” “No it doesn’t, but it does require partial function application!” “You’re both wrong, but it does require a powerful type system!” “What does ‘powerful’ mean?” and so on.

              So instead you say “well when I think of my program in terms of functions, I find that I can write the code that matches my mental model really easily in X language, so I say it’s functional for me!”

              Honestly, part of why I like this is that I think it helps us get away from an endless and unsolvable definitional fight and into the more interesting questions of how features intersect to increase of decrease comfort with common mental models.

              1. 2

                Honestly, part of why I like this is that I think it helps us get away from an endless and unsolvable definitional fight and into the more interesting questions of how features intersect to increase of decrease comfort with common mental models.

                I love how the other comments in this thread back this up. People are arguing over “no, a functional language must have X” / “no, it means Y” and they’re never going to agree with each other. Just acknowledge that fact and move on!

              2. 3

                For the repl: Give https://github.com/google/evcxr/blob/master/evcxr_repl a chance. I just became aware of that project recently (the fact that they have a standalone repl) and have not yet tried to push it. It’s certainly appears not that full featured compared to dynamic languages or ghci but it should be good enough to make an inferior lisp mode out of it.

                1. 11

                  Thanks but if the language doesn’t have a repl built-in it’s a good sign that it’s creators don’t value any of the same things I value in a language, so I don’t think rust would be a good fit for me.

                  1. 3

                    Never change, Technomancy ❤️

                2. 3

                  This is absolutely true, IMO. And the same can be said with OOP.

                  A “functional language” would really be any language that strongly encourages a functional style or truly forbids an OOP style. I think Haskell and Elixir are pretty close to forbidding OOP programs.

                  Likewise, an OOP language is one that strongly encourages an object oriented style. JavaScript is OO because even functions are objects and you can add behaviors and properties to any object.

                  Etc, etc.

                  But I’m a bit confused by your comment about statements. Rust is pretty expression oriented. if is an expression, match is an expression, loops are expressions that return the value of its final iteration, all functions return the final expression in its body as the return value, etc.

                  1. 2

                    I think Haskell and Elixir are pretty close to forbidding OOP programs.

                    In what way do you see that? I guess it depends what you mean by “OOP” of course but Haskell has several powerful ways to do OOP-style message passing and encapsulation.

                    1. 1

                      I’m sorry for the confusion. Everyone means something different when they say “OOP” (and “FP”). When I said OOP, I meant a style that revolves around “objects”. In my mind an object is something that has hidden, mutable, state. A black box, if you will. You may call a method on an object (or send it message), but you are not necessarily guaranteed the same response every time (think sending an HTTP request, a RNG, even a mutable Map/Dictionary is an object per my defiinition).

                      I’ve never used Haskell for serious work, so I could’ve been totally off base there. And, actually, I guess Elixir does have message passing between processes. I was only thinking of the code inside a process… So, I’m probably wrong on both counts!

                      1. 1

                        Just as an example, here’s one way to do mutable student message passing style in Haskell:

                        https://paste.sr.ht/~singpolyma/c618d894e7493d7197ef745035a8691d53e2a193

                        (This is an example and a real system would have to handle a few cases this does not.) In this case it’s a monitor (threaded and threadsafe) and you can’t do inheritance (but you can do composition).

                    2. 1

                      A “functional language” would really be any language that strongly encourages a functional style or truly forbids an OOP style.

                      Gonna have to disagree here; whether something encourages object oriented programs or functional programs should be thought of as two orthogonal concerns that simply happen to be correlated in most widely-used languages. That said, “OOP” is such a poorly-defined term that I’m not even sure it’s worth spending any effort untangling this; IMO the term should be completely abandoned and more specific terms should be used in its place, like “message-passing”, “inheritance”, “encapsulation”, etc. (For instance, Elixir has great support for message passing and encapsulation, two cornerstones of what is often called “OOP”. No inheritance, but that’s great because inheritance is a mistake.)

                      But I’m a bit confused by your comment about statements.

                      I looked into it and … it IS confusing! Rust says that they “have statements” but what they really have is expressions that return Unit. Calling that a statement is pretty misleading IMO, because every other language I know that “has statements” means something completely different by it.

                      1. 2

                        That said, “OOP” is such a poorly-defined term that I’m not even sure it’s worth spending any effort untangling this; IMO the term should be completely abandoned and more specific terms should be used in its place, like “message-passing”, “inheritance”, “encapsulation”, etc. (For instance, Elixir has great support for message passing and encapsulation, two cornerstones of what is often called “OOP”. No inheritance, but that’s great because inheritance is a mistake.)

                        Yeah, it’s definitely poorly defined. And my examples of Haskell and Elixir were actually bad examples. When I think of OOP, I’m thinking about black boxes of (potentially) mutable state and “message passing” (which may just be calling methods). You can’t, in theory, expect to get the same “response” if you send multiple messages to an object.

                        As you said, Elixir is a good example of both FP and OOP. Kind of OOP in the large, FP in the small.

                        Apologies for the confusion.

                        I looked into it and … it IS confusing! Rust says that they “have statements” but what they really have is expressions that return Unit. Calling that a statement is pretty misleading IMO, because every other language I know that “has statements” means something completely different by it.

                        Yeah, that’s strange that Rust docs would say they have statements… Maybe assignment is a statement? I don’t know. Most stuff in Rust is expressions, though- even the for loop that always returns Unit/void/whatever. It’s a neat language. Definitely not (pure)-function-oriented, IMO, but really fun, ergonomic, and safe for a systems language.

                        I think the Rust docs also used to say that it’s not OOP. I think that’s wrong, too. It doesn’t have struct/class inheritance, but I think that you can get really far with an OOP style in Rust- struct fields are private by default; mutation is controlled via the borrow checker; traits are like type classes and if you write “object-safe” traits, you can pass trait objects around.

                    3. 1

                      I’m increasingly coming around to the conclusion there’s no such thing as a functional programming language

                      The way I see it, a functional programming language is a one that maintains referential transparency.

                      Lambda calculus, Haskell, Elm, Purescript, Futhark, Agda, Coq, and Idris are some examples.

                      Then, languages which don’t enforce referential transparency fall on the scale of “more” or “less” functional, based on how easy it is to write pure code, and how frequently it is done in practice.

                    4. 12

                      I think I get what you’re saying here, but I’d say you’re mixing up mechanisms and goals.

                      The goal is “safe concurrent programming”. Lack of race conditions, etc. There are (at least) two different mechanisms for that:

                      • immutability, as in functional programming languages (Clojure, Haskell, etc.)
                      • ownership, as in Rust

                      Just because Rust has the same goal as a functional programming language, but uses a different mechanism, doesn’t make it functional!


                      This is basically the viewpoint here, and I talked with another Rust programmer on HN who agrees: Rust is procedural, but it’s safe with respect to concurrency. (It’s also memory safe, but so are Java and Python, which are also procedural in the sense that idiomatic code often relies on mutation.)

                      https://boats.gitlab.io/blog/post/notes-on-a-smaller-rust/

                      As I said once, pure functional programming is an ingenious trick to show you can code without mutation, but Rust is an even cleverer trick to show you can just have mutation.

                      1. 4

                        Thank you for your reply! Now Rust functions that are not requiring exclusive access to any parameter and don’t use cells or unsafe are referentially transparent. Of course that is not unique to Rust. If you stick enough consts in weird places into a C++ function you can achieve similar things. But the Rust compiler “rewards” that by requiring less strict conditions on parameter / context when calling such a function. That is one part I wanted to convey.

                        But I understand and fully agree with you that Rust provides a language mechanism that is different than what FP languages offer and which has different trade-offs. Thank you in particular for the link. I’ll update the post tomorrow and will link / quote your comment as this adds an important aspect I did not consider.

                        1. 4

                          100% agree!

                          Rust solves the same problem(s) that functional programming solves, but in a different (better, more performant) way.

                          I think it’s weird that people mention immutability as an example that makes Rust “functional”. It’s the exact opposite! Rust has “controlled mutation”. It’s perfectly safe (and often preferred) to pass by mutable reference in Rust. That’s expressly NOT functional because any function that takes a mut ref is not pure.

                          It’s the borrow checker that PRECLUDES (idiomatic) Rust from being functional, IMO.

                          1. 1

                            Yes, exactly, and this is where I see similarities between Rust and Clojure. Clojure (also not a pure FP language but leaning very heavily on immutability) provides different means to control change (atoms, transactions, agents, channels), which are mostly different from the change control the Rust offers (cells are sort of atoms but the borrow checker is of course in a different category). Even though those mechanisms are different, they can influence the design of programs in sort of similar ways.

                            1. 1

                              I did a small project in Clojure a few years ago (while spec was just about to be released as an experimental feature), so my memory of the language might be a little fuzzy.

                              What about Clojure makes you feel like it’s not a pure FP language? It doesn’t technically prevent side-effects, but does any language besides Haskell actually do that?

                              I felt like all other aspects of it “count” as functional. In particular, I don’t remember being able to mutate a function’s input parameters through its reference. I could be mistaken though, because I probably didn’t want to do that… :D

                              1. 2

                                Can’t speak for them, but when people call a language “purely functional” they often mean pure as in purity - the specific property of the language not permitting side effects, randomness, or mutation. Not as in “all-in on being functional” or “very functional”

                                1. 1

                                  That’s fair, but then why do they spell “Haskell” that way? :P

                                2. 1

                                  As became apparent with many of the replies here, the definition for whats a functional PL can somewhat differ :) The list of the wikipedia article https://en.wikipedia.org/wiki/Functional_programming is fairly inclusive and contains Clojure. In practice, idiomatic Clojure is very much focused on pure, immutable functions. At the same time it defines itself as a host language - running on top of Java / JavaScript C# (once upon a time). It allows to directly call stateful methods using the interop features and especially in ClojureScript I have seen pretty stateful programs. For that reason and for the audience here, the qualification.

                            2. 2

                              I’ve had this realization myself, and so I made a little chart to help convey it: http://paste.debian.net/1183860/

                              I hope people find it useful.

                            3. 3

                              This post is so infuriating (in a light-hearted way): it comes so close and yet misses the main idea, imo :-)

                              “Rust doesn’t have immutable data structures in std” is literally true, but irrelevant. In rust, persistent and mutable data structures have exactly the same API, only big-O is different. You don’t need to restrict Rust to shared references to get referral-transparency like properties, &mut is no less “functional” than move or & (additionally, & does not actually guaranty referential transparency, there are things like eprintln)

                              This is nicely explained in http://smallcultfollowing.com/babysteps/blog/2018/02/01/in-rust-ordinary-vectors-are-values/, but it feels like that post is in dire need of a more clickbaity title :)

                              1. 2

                                I have to say, I really don’t understand the point you’re making with that post and the sentiment. I don’t mean that in a combative way.

                                Isn’t the point of persistent structures that it looks and behaves as though you’re mutating, but that the performance characteristics are different?

                                What’s the point of saying that the standard collections have the same API as persistent collections? That’s just the hallmark of a good persistent collection. The entire point of it is to have cheap copies, not to have different observable behavior.

                                But, I will say, that the fact that Rust doesn’t do persistent structs or collections in the std lib is another reason it should be pretty clear that it’s not a functional language. A functional language should probably default to persistent structures.

                                1. 2

                                  Isn’t the point of persistent structures that it looks and behaves as though you’re mutating, but that the performance characteristics are different?

                                  I don’t think so. This use-case for persistent data structures is very niche. I’ve seen a lot of Rust code, but, only Cargo (for backtracking SAT solving) and rust-analyzer (for syntax trees) needed their specific performance characteristics.

                                  A much more important property of persistent data structures for day-to-day programming imo is value semantics. It’s easy to see with the simplest possible functional data structure – a record. An

                                  struct Person {
                                    first_name: string,
                                    last_name: string,
                                  }
                                  

                                  can either be mutable (allowing to set the fields directly) or immutable (with with_first_name method which returns a copy of the struct). Performance is O(1) in both cases. However, there’s a popular school of thought that says that immutable version is much more preferable, because it makes application-level programming simpler, because immutable things are values. I think this valuness is the thing that makes persistent collection widely relevant.

                                  The catch is, persistent data structures usually don’t look like like mutable ones. One writes

                                  let (new_xs, x) = xs.pop()
                                  

                                  rather than just

                                  let x = xs.pop()
                                  

                                  Except in Rust, where it’s let x = xs.pop() either way.

                                  Hand waving, rust fn foo(x: &mut T) -> U is a syntactic sugar for fn foo(x: T) -> (T, U).

                              2. 2

                                There is another interesting term, which is maybe easier to define: “expression-based programming”.

                                In Haskell, if you sequence some actions, where each action depends on the previous, you can use do-notation (which even works if you’re not working with a monad). And even though it spans multiple lines and has no braces around it (like in lisps), it is still an expression.

                                In Pure FP such as Haskell, people refer to “referential transparency”. You can refactor with the monad laws (even if they haven’t been proven), because the sequential-looking code means that things like the monad bind function is available, even if it wasn’t mentioned in the sugared code. You can be sure that even “effectful” code can be manipulated as just another expression.

                                Sure, you can refactor Rust code pretty well, but since there are no higher-kinded types, you will have to keep those 30 versions of what in Haskell is called “traverse”; often it will be a a couple of nested loops. But you can’t treat a for-loop as an expression. You won’t even see that the code is duplicated since the inside of the loops will be different.

                                From a Haskeller perspective, I like the term “expression” over “function” because if I have such a series of actions, the type of that do-expression won’t be a function! Haskell is lazy, so there is less emphasis on “calling” as function, as the value won’t be computed until necessary anyway. But everything is indeed an expression, which it isn’t in other languages. So isn’t it the more general description of the phenomenon?

                                You could even argue that the term covers lisps, since their s-exps can be introspected and quoted, giving you the opportunity to treat even imperative Clojure do-notation as an expression, before it is executed.

                                1. 1

                                  But you can’t treat a for-loop as an expression. You won’t even see that the code is duplicated since the inside of the loops will be different.

                                  I think you might have just not been precise with your wording, but in Rust, “for” is an expression. It evaluates to the value of the last iteration it computes.

                                  It must be that you’re using the word “expression” differently than in the sense of “expression vs. statement”.

                                  EDIT: I stand corrected. @notriddle pointed out that for loops always return void as an expression (still technically is an expression, I guess). You can return non-void values from loop {}, though.

                                  1. 1

                                    No it doesn’t. It evaluates to (). Always.

                                    1. 1

                                      Oh, shucks. My memory was fuzzy. I was pretty sure I had returned values out of for-loops before, but you’re right.

                                      I was thinking of this feature: https://doc.rust-lang.org/edition-guide/rust-2018/control-flow/loops-can-break-with-a-value.html

                                      I’m not sure where I came up with “returning the last value of the iteration”. Maybe another language I was working in recently does that.. /shrug/

                                    2. 1

                                      I guess the term would be even better if there were a word like “expression” which also meant that it was introspectable and parameterized. Is it “function”? ;) Maybe this is why people like the term “FP”; in Haskell a loop, even after (partially) applying the body, actually is still a function. I guess you can manipulate loop contents in Lisps and Rust too, but you have to use macros… If you allow macros, almost everything becomes an expression, but it really is a different language, so maybe it shouldn’t count. Or maybe it should, since it is always available and switching between the two is second-nature for some.

                                      1. 2

                                        I guess the term would be even better if there were a word like “expression” which also meant that it was introspectable and parameterized.

                                        When I first learned Racket I was confused as to why they kept calling functions “procedures” but then I realized it was because of this. Maybe “mathematical function” would be a more precise term for what you’re looking for? I agree “expression” is way too overloaded.

                                  2. 2

                                    Rust is (to me) one of the “New ML” family. Others in this family would be Swift or Scala

                                    Rust is the one of this family that brings somethng new and useful to the table of course in the form of compile time memory management.

                                    1. 2

                                      What’s great about this blog post and the discussion I’m seeing here is that it’s making us think about what “functional” really means. Not to get all “functional programming is the friends we made along the way”, but…

                                      There is such a thing as a Functional Programming Language, with all the properties of purity, higher order functions, referential transparency, only functions for control flow, etc. This term’s meaning comes from the mathematical construction of the language, and its specific properties that result from the language rules.

                                      But what’s more interesting to me is the practice of “functional programming” - taught to us by our use of Functional Programming Languages - which pursues the value we got from referential transparency, purity, Hindley-Milner type systems, or s-expressions, or whatever else. This idea of “functional programming” is what the programmer does when they designing a program around these features. So in this sense a language can encourage “functional programming” without needing any such classification. The fact that rust isn’t a Functional Programming Language, but instead gracefully allows for some of the best parts of functional programming while also permitting imperative systems-level programming is what makes it unique as opposed to checking the box of yet another Functional Programming Language.

                                      1. 1

                                        I’m very pleased that you at least discussed what you mean by “functional”. Everyone who even begins to discuss whether something is “functional” or “object oriented” or “procedural” or whatever, should be required to begin with a definition of what those things mean to them.

                                        Your post makes me feel mixed things about whether Rust is “functional”.

                                        The borrow checker means that you are free to write impure functions without sacrificing correctness/safety. It’s also more performant to do so. So why would you write functional style Rust? You’re sacrificing (a little) performance for no real gain in correctness.

                                        Granted, I agree with your point that Rust’s controlled mutation mechanism does encourage you to write less mutation-happy code because passing around too many mutable references gets painful compared to other languages with no mutation control.

                                        I’d say that Rust, fundamentally is not a functional language. Taking a copy when you could mutate would be something of an anti-pattern, IMO.

                                        It’s just that working with the borrow checker will make you want to write code with less mutation. On the other hand, writing Java code ALSO makes me want to write code with less mutation because of how buggy and incomprehensible my code becomes with prolific mutation. Is Java functional? Probably not.

                                        1. 3

                                          Funnily enough when you are first starting out with Rust a common way to make the borrow checker happy is liberal use of .clone(). Eventually you grow out of it. But in a way this supports the authors point in that immutable copies is a somewhat natural if less performant style that the borrow checker pushes you towards.

                                          1. 1

                                            Interesting point!

                                        2. 1

                                          Speaking as a professional user of Haskell (5 years) and Rust (2 years) Rust isn’t a functional programming language but that’s okay.