Functions—chunks of code that can complete a given task again and again within a program, without the need to re-write the code over and over—are a standard part of programming.
On the one hand, this article clearly feels like it was written with a fortune-500 VP as the intended audience. On the other hand, it’s very weird to see things like nix, haskell, and clojurescript being explained to someone who doesn’t know what a function is.
I guess that’s what you get when someone tries to write a somewhat technical article but the marketing team decides it needs to be made more “accessible”.
My motivation was moreso that state is present regardless of which you use, it’s just hiding behind the interface of those functions. I have no qualms about writing a for-loop :)
If you use them under the hood sure. I just don’t like reviewing code with reduce in it because it takes me longer to figure out what it’s doing than if it was a for loop modifying a variable.
A for loop modifying a variable doesn’t have any less state than a fold. Arguably, it has more. Fold forces the stateful parts of the routine to be isolated and made explicit.
That’s a good argument for what I said about statelessness (which I agree was not well put) but you’re responding to my better argument that it’s really about readability.
The way I like to think about it, there’s reduce, and there’s fold. reduce is a fold where the operation is monoidal – theer’s a neutral element, and the operation is associative (so the directionality of fold doesn’t matter, it can even be computed in parallel). This reduce is good and readable: sum(xs) = reduce(+, xs). The general fold, where directionality matters, is indeed unreadable. Sadly, few languages provide reduce without providing fold.
Recursion is useful (try writing a parser without it!), but tail recursion can take a hike. If you don’t need to keep unique state over the tree, that’s a good indicator that recursion is overkill for your usecase.
Functional programming is going mainstream in the same way as object oriented programming did: by losing the dogma. None of the mainstream OO languages are pure OO. They have types that are not objects, they typically have something that approximates free functions, they just provide tools that allow encapsulation and writing code that abstracts over data types. Similarly, the FP languages that are going mainstream have side effects and have mutability, they just favour a style that encourages immutability and keeping data structures defined concretely while abstracting over operations on them.
As someone who likes both Smalltalk and Haskell (which I consider examples of pure OO and FP languages, respectively), I wouldn’t use either for a large project because both remove things that are valuable in specific domains and large projects tend to span multiple problem domains.
Many years ago due to poor decision making I was writing a Haskell compiler that targeted .NET, and one of the big perf improvements was converting tail calls into loops* (in many cases easier to understand than recursion in my view) using mutable values. Part of doing this was using CodeDOM or whatever it was called - the attempt at generic XML representation of languages or some such - and I’d check the C# output, and in my opinion the local mutable state was much more comprehensible.
I think people attached to the “no mutable state” underestimate the mental cost of some simple tasks when mutable state is removed, and I think they overestimate the cost of mutable state - especially local mutable state, but also just in general. I also think there’s a general failure to acknowledge that this is a one way relationship - I can easily write code in an imperative language that doesn’t make use of mutable state, but I can’t write code in a pure functional language that has clean mutable state.
* IIRC at the time .NET’s VM did have a tail call instruction but it was purely for the semantics, the performance was horrific. Again IIRC it was heap allocating the security frames, so each tail call would not recurse and use the machine stack but there’d be a heap allocation for the security stack. I think. Happily you can also emulate arbitrary recursion by catch stack overflows and continuing on a new thread.
Seems reasonable that a lot of code should be written functionally. But we have to be specific where it’s best suited. Not sure if I’m representing the balanced, trade-off argument correctly:
A lot of software doesn’t need to be optimized for speed, but for reducing bugs and and increasing developer productivity. That’s what I hear implicitly in the article. Not being able to checkout a cart is fatal on a commerce app. A few extra milliseconds is not a big deal to use immutable data for a such a web app. Copying data to prevent mutation is more resource intensive. I understand it’s more complicated than that sometimes but generally that’s what’s happening, correct? Pointers/references vs copying. It’s a trade-off mostly felt by systems programmers in high-performance environments where the data must be as close to the processor as possible.
That’s the trade-off which probably gets the most discussion around it, but I think the much more important trade-off is that of functional-core/imperative shell. You move as much logic as you can into the functional core where it’s trivial to test, easy to understand, and harder to introduce bugs, and you keep the side-effects at the edges of your system where they don’t leak into the rest.
The speed argument is just not that relevant IMO to the majority of code that developers write, plus decent functional languages give you immutability without copying most of the time. With generational GC the performance cost is much lower than most people expect.
On the one hand, this article clearly feels like it was written with a fortune-500 VP as the intended audience. On the other hand, it’s very weird to see things like nix, haskell, and clojurescript being explained to someone who doesn’t know what a function is.
I guess that’s what you get when someone tries to write a somewhat technical article but the marketing team decides it needs to be made more “accessible”.
Parts of functional programming that I still like: everything is an expression (especially “if”) , map/filter, constant values by default.
Parts of functional programming I’m over: recursion for everything, reduce, variable shadowing. I’m sure there’s more but I can’t remember right now.
Separate from this is a discussion of type systems. But expressive type systems and functional programming have no real connection.
I’m curious why you like map/filter but dislike fold/reduce?
Most recursion should be invisible anyway
Map and filter are stateless, basically.
I’m curious what you think of these:
map f as = reduce (a bs -> f a : bs) empty as
filter p as = reduce (a bs -> if p a then a : bs else bs) empty as
That’s just an imperative way to write the same thing.
That’s my point. Saying “you can implement map as a reduce” isn’t any more insightful than saying you can implement it as a for loop.
But then why would having
reduce
be worse than having for loops?The conversation was about why @eatonphil likes map but not reduce, “even though” you can implement map with reduce.
My motivation was moreso that state is present regardless of which you use, it’s just hiding behind the interface of those functions. I have no qualms about writing a for-loop :)
If you use them under the hood sure. I just don’t like reviewing code with reduce in it because it takes me longer to figure out what it’s doing than if it was a for loop modifying a variable.
A for loop modifying a variable doesn’t have any less state than a fold. Arguably, it has more. Fold forces the stateful parts of the routine to be isolated and made explicit.
That’s a good argument for what I said about statelessness (which I agree was not well put) but you’re responding to my better argument that it’s really about readability.
I have written many KLOC of FP and recursion was rarely needed. It’s cryptic and not very declarative.
I like point-free style as in: https://pages.cpsc.ucalgary.ca/~robin/class/449/Evolution.htm
The way I like to think about it, there’s
reduce
, and there’sfold
.reduce
is afold
where the operation is monoidal – theer’s a neutral element, and the operation is associative (so the directionality of fold doesn’t matter, it can even be computed in parallel). Thisreduce
is good and readable:sum(xs) = reduce(+, xs)
. The general fold, where directionality matters, is indeed unreadable. Sadly, few languages providereduce
without providingfold
.Recursion is useful (try writing a parser without it!), but tail recursion can take a hike. If you don’t need to keep unique state over the tree, that’s a good indicator that recursion is overkill for your usecase.
Functional programming is going mainstream in the same way as object oriented programming did: by losing the dogma. None of the mainstream OO languages are pure OO. They have types that are not objects, they typically have something that approximates free functions, they just provide tools that allow encapsulation and writing code that abstracts over data types. Similarly, the FP languages that are going mainstream have side effects and have mutability, they just favour a style that encourages immutability and keeping data structures defined concretely while abstracting over operations on them.
As someone who likes both Smalltalk and Haskell (which I consider examples of pure OO and FP languages, respectively), I wouldn’t use either for a large project because both remove things that are valuable in specific domains and large projects tend to span multiple problem domains.
Many years ago due to poor decision making I was writing a Haskell compiler that targeted .NET, and one of the big perf improvements was converting tail calls into loops* (in many cases easier to understand than recursion in my view) using mutable values. Part of doing this was using CodeDOM or whatever it was called - the attempt at generic XML representation of languages or some such - and I’d check the C# output, and in my opinion the local mutable state was much more comprehensible.
I think people attached to the “no mutable state” underestimate the mental cost of some simple tasks when mutable state is removed, and I think they overestimate the cost of mutable state - especially local mutable state, but also just in general. I also think there’s a general failure to acknowledge that this is a one way relationship - I can easily write code in an imperative language that doesn’t make use of mutable state, but I can’t write code in a pure functional language that has clean mutable state.
* IIRC at the time .NET’s VM did have a tail call instruction but it was purely for the semantics, the performance was horrific. Again IIRC it was heap allocating the security frames, so each tail call would not recurse and use the machine stack but there’d be a heap allocation for the security stack. I think. Happily you can also emulate arbitrary recursion by catch stack overflows and continuing on a new thread.
One of the most popular OO languages is Ruby. Nothing in this world is pure, but Ruby is pretty close for being so popular.
Ruby is still fairly niche. Compared to Java, JavaScript, and C# in monthly lines of code changed, it’s hard to see where the Ruby line leaves the x axis a lot of the time. That said, JavaScript is pretty close to a pure OO language and is very popular.
Fun fact: so did GitHub, which is strangely not mentioned in the article.
Here’s a recent comment about the state of Semantic at GitHub from the manager of the team. Maybe that’s why…
Seems reasonable that a lot of code should be written functionally. But we have to be specific where it’s best suited. Not sure if I’m representing the balanced, trade-off argument correctly:
A lot of software doesn’t need to be optimized for speed, but for reducing bugs and and increasing developer productivity. That’s what I hear implicitly in the article. Not being able to checkout a cart is fatal on a commerce app. A few extra milliseconds is not a big deal to use immutable data for a such a web app. Copying data to prevent mutation is more resource intensive. I understand it’s more complicated than that sometimes but generally that’s what’s happening, correct? Pointers/references vs copying. It’s a trade-off mostly felt by systems programmers in high-performance environments where the data must be as close to the processor as possible.
That’s the trade-off which probably gets the most discussion around it, but I think the much more important trade-off is that of functional-core/imperative shell. You move as much logic as you can into the functional core where it’s trivial to test, easy to understand, and harder to introduce bugs, and you keep the side-effects at the edges of your system where they don’t leak into the rest.
The speed argument is just not that relevant IMO to the majority of code that developers write, plus decent functional languages give you immutability without copying most of the time. With generational GC the performance cost is much lower than most people expect.
100% agree.