Functional Language Features: Iterators and Closures
doc.rust-lang.org
doc.rust-lang.org
> ...
> 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?
(define (f x)
(match x
[`(,x ,x ,y) y]
[_ #f]))The procedural 'if x.len == 3 and x[0] == x[1]' seems a lot clearer as to what is actually being tested.
In Rust, you'd write something more like:
fn f(x: &[i32]) -> bool {
match x {
[x, y, _] if x == y => true,
_ => false,
}
} fn f(x: &[i32]) -> bool {
x.len() == 3 && x[0] == x[1]
}
if you wanted. Honestly, I probably would.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.
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.
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!)
What they lacked was usually a nice way to operate on tagged unions, ie. pattern matching.
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.
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)
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
}- 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.
$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...
// 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"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...
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.
In that case you're passing around a function, which matches how this chapter characterizes "functional programming".
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.