Hacker News new | ask | show | jobs
Functional Language Features: Iterators and Closures (doc.rust-lang.org)
62 points by s_c_r 2183 days ago
5 comments

> Programming in a functional style often includes using functions as values by passing them in arguments, returning them from other functions, assigning them to variables for later execution, and so forth.

> ...

> Other Rust features, such as pattern matching and enums, which we’ve covered in other chapters, are influenced by the functional style as well.

What is it about pattern matching and enums that associates them with functional programming? Because going by the description of functional programming above, I don't see how they fit in. Is it just that pattern matching and enums were first popularized by certain functional languages?

> first popularized by certain functional languages

Basically that.

People associate all kinds of different thinks with the term functional programming. The only think in common is that it's focused on functions, including first class functions.

For other thinks including things like (strict) immutability, lazy evaluation, syntax, algebraic data types, linear typing, etc. it is different from person to person weather or not they think about this as part of FP or just another think which happens to be part of the FP language they use/looked into.

A lot of those things are natural consequences of having functions without side effects though. Lazy evaluation for instance, can be bonkers if your functions have side effects.
It's not only 'first' but also immutable as a way of life.

You walk structures to extract information and derive a new structure. With immutable types and automated constructors this becomes regular enough to be baked in the language. Other languages like memory mutation so the main action is setting a variable to modify it.

> Is it just that pattern matching and enums were first popularized by certain functional languages?

Not only were Sum Types first popularised by functional languages, most imperative/OO languages still don't have them (although I'm not quite sure why not). E.g. none of Java/C#/C++ have them. Neither do Python/Ruby/JavaScript/PHP (although the need is somewhat reduced in dynamic languages)

> Neither do Python/Ruby/JavaScript/PHP (although the need is somewhat reduced in dynamic languages)

I fully agree. Pattern matching is specifically useful for statically typed languages, where you can determine whether the patterns are exhaustive. Here pattern matching plays really nicely with the premise of having statically typed consistency guarantees.

In dynamically typed languages there is a use-case which makes sense: You want to conditionally extract nested values. But typically you have destructuring and a huge generic tool-set of predicates and transformation functions to do this. The advantage of being dynamically typed is how those things freely compose at the cost of having fewer run-time guarantees.

Ruby has pattern matching as an experimental feature: https://www.ruby-lang.org/en/news/2019/12/25/ruby-2-7-0-rele...
C++17 does - it's called std::variant.

  std::variant<int, bool, double> options;
  options = true;
  
  bool value = std::get<bool>(options);
  bool has_bool = std::holds_alternative<bool>(options);
  
  // or test which alternative is held
  if (auto i = std::get_if<int>(&options)) {
    // do something with int
  } else if (auto b = std::get_if<bool>(&options)) {
    // do something with bool
  } else {
    // do something with double
  }
That's not a language feature though, right? It's just a struct containing a union and a discriminant. That means:

- No pattern matching means your stuck with the awkward if-elseif-else

- It doesn't check that you've accounted for every possible variant.

- You can only hold one variant of each type: you can't have two variants that both contain a string.

- The specific instance isn't it's own type, so you can't implement methods on it.

A language that I am working on has those things "built-in" (it still generates compilable C++ in the backend). I did recently add pattern matching (and not just on variants) where the above would be equivalent to:

  $v:|[:int, :bool, :double] = 5;

  $vi: = v.[:int];
  $holds_int: = v.?[:int];

  switch v {
    .[:int]$i { /std cout << "got int:" << i };
    .[:bool] { /std cout << "got bool" };      
    .[:double]$d { /std cout << "got double: " << d };
  };
If a type repeats in the same variant, you would match by index to tell them apart:

  $v:|[:int, :int]<(.[0] = 5);

  switch v {
    .[0]$i0 { /std cout << "got first int:" << i0 };
    .[1]$i1 { /std cout << "got second int: " << i1 };
  };
(I have some ideas to allow named labels instead of numerical indices but that is not yet implemented)

It is, in spirit, an implementation of the inspect proposal [1], but with a (subjectively) much simpler and more powerful syntax (the grammar of the entire language is fully LALR(1) without ambiguities).

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p137...

Take a look at my reply above. Your right it isn't built into the language, but it does come with a helper method, std::visit, which does take into account that you've accounted for all possible variant types, and will throw a compile error if you didn't. You also aren't stuck with if, else-if style syntax.
You can use std::visit() for the time being, and pattern matching is being discussed.
This works out really nicely when combined with variadic template tricks. (Taken from CPP Reference)

  // Helper for creating anonymous visitor functions
  template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
  template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

  using var_t = std::variant<int, long, double, std::string>;

  std::vector<var_t> vec = {10, 15l, 1.5, "hello"};

  // Type matching visitor
  for (auto& v: vec) {
        std::visit(overloaded {
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
        }, v);
    }
Outputs: 10 15 1.500000 "hello"
That does not give you destructuring though, so to distinguish different variants of the same type you are left with a classic `if` inside the lambda.

Also this has "language support" in the sense that it is (again) implemented via template meta programming, i.e. the compile time is impacted quite heavily.

See "std::visit is everything wrong with modern C++": https://bitbashing.io/std-visit.html

This standards proposal would add proper match (named, of course, inspect) support: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p009...

Yes, I'm definitely not saying that std::visit is perfect by any stretch of the imagination
What a ridiculous language C++ is.
One of my teachers liked to use the term value-oriented programming for the FP subset that does not make use of functions for data representations, or even first class functions beyond combinators like 'map'. I've always liked that term and that concept, and I do believe value-oriented programming, understood as programming with pure functions and simple values as much as possible, is 75% of the value of full functional programming. And I say that as someone who has both used Haskell as my primary language for over a decade, and implemented my own functional language.
My take: sum and product types exist in various type theories (enums with their elimination rule being pattern matching). Some of the first type theories to feature them were for the lambda calculus, which functional programming is based on. As a result, they've been ubiquitous in functional programming, and have become associated with it heavily.
Rust enums aren't like C style enums. They are a kind of algebraic data type: a sum type. Sum types haven't, until recently, been very well represented outside of functional programming languages. In haskell for example you might have a function that accepts a sum type, and to handle it you pattern match each variant and provide an implementation for that variant.

This is pretty core to functional programming languages like haskell, and its very similar to how rust behaves, and is omnipresent much like in haskell. For example, rust does not have exceptions, but repersents errors via the `Result` enum which is analogous to Haskell's `Either` data type. In order to access any value wrapped in an Result you must pattern match on it and explictly handle the possibility of an error.

All of this comes from functional programming languages, but they are starting to become common place in mainstream languages.

Yes -- sum types have historically not been present in imperative languages, whereas products types have essentially always been present. Imperative languages have correspondingly good support for various uses of product types, but even if sum types can be encoded, you generally can't abstract well over them due to the nonexistent language support for their use.

C does have unions, but you can't distinguish which field is safe to read without additional information. Discriminated unions are a design pattern built on top of the language feature to give a true sum type, but since they aren't core to the language, pattern matching still isn't a thing.

(Technically, "pattern matching" encompasses both dispatching on sums and destructuring assignment on products, and to be fair, not a lot of imperative languages have good support for the latter either. But many do!)

Historically imperative languages did have tagged unions (Algol, Pascal, Ada, Modula). Their disappearance is mostly because of OOP's takeover I think. It was thought that a tagged union should be replaced by a base class with one derived class per variant.

What they lacked was usually a nice way to operate on tagged unions, ie. pattern matching.

Thanks! Always interested in learning more about the historical PL landscape ^_^
They make it much easier to do certain kinds of things as expressions (as opposed to, say, an if/else chain which in many languages is a statement and not an expression, or a switch statement which I don't think is ever an expression)
In general the article is woefully imprecise. Yes, Rust does support higher order functions and other aspects of functional programming, but closures can have side effects. There is no concept of referential integrity in the language (I wish there was something like a “pure” keyword). Don’t get me wrong, I love Rust, but in the scheme of things, it’s a very imperative language.
Functional programming is philosophically aligned with purity and immutability. When you have enforced immutability, many of the features of OO programming disappear, and you're left with having pointers to a bundle of values with various getter style methods. In order to recover the ability to make complex data structures, you need to encode variants. This can be done with Null and the bundle of values, or it can be done with Algebraic Data Types, aka Rust's fancy enums.
I think variant records (aka 'enums' in Rust) were first popularized by Pascal. They were in ALGOL 68 even earlier, but that language wasn't widely used.
I believe, on the pattern matching side, it has to do with the fact that it's a higher level feature that feels much more declarative. Inside of writing a sequence of instructions to check whether a list is 3 and the first two are equal, one writes the declarative syntax:

  (define (f x)
    (match x
      [`(,x ,x ,y) y]
      [_ #f]))
I'm sure it makes sense to someone well-versed in Rust syntax, but to an outsider like me that code looks like the kind of "line noise" that made me switch from perl to python.

The procedural 'if x.len == 3 and x[0] == x[1]' seems a lot clearer as to what is actually being tested.

That code in the comment above isn't actually rust. It's a lisp, I believe.

In Rust, you'd write something more like:

  fn f(x: &[i32]) -> bool {
      match x {
          [x, y, _] if x == y => true,
          _ => false,
      }
  }
... and you could write

  fn f(x: &[i32]) -> bool {
      x.len() == 3 && x[0] == x[1]
  }
if you wanted. Honestly, I probably would.
A) this is in Racket, as that's the editor window I had up. B) your point is fair, one benefit of this is that it also binds names to things.
pattern matching comes along with discriminated unions (enums). discriminated unions come along with functional programming, probably because they give you a more complete set of types mathematically (AND types as we are used to with structs/classes and OR types)
Why do people post chapters from manuals to HN?
To start a discussion about that topic while bringing users who are unfamiliar up to speed quickly?
Maybe. Of course, everyone can submit whatever they want. To me it looks strange people upvote such links.
How are iterators functional language features? They're not unique to functional languages and they don't require function composition to implement. They're present in most popular imperative languages.
Even if it's not the only way to implement iterators, using closures which maintain state and yield values one after another seems to be a very popular way to do it.

In that case you're passing around a function, which matches how this chapter characterizes "functional programming".

I've tried to use iterators and closures in my code, but it's not as easy with error handling and lifetimes. Like I could figure out the magic incantation that lets me map over an iterator with a function that returns a result. Or I could write a for loop that pushes to a Vec.

Or I could chain an ok_or_else on an Option but ugh now Rust is complaining that I'm capturing a reference to self. Screw it, I'll rewrite it to be an if let with a return. Part of the problem there is that we know an ok_or_else with try! will execute the closure and return if the value is None, but Rust's borrow check doesn't know that.

None of this is Rust's fault. It's just that it's hard to combine ergonomic closures and borrow checking.

as a side note : I hate this ferries thing, just write a good old comment. what's wrong with comments?