Functional programming is more of a spectrum; not an "all or nothing" concept. You can write FP-style programs in non-FP languages. (And you can use non-FP style in FP languages.) Understanding FP will help you write better non-FP code.
Simple example: I have switched from declaring an empty array [], then filling it via a loop to declaring the same array with values from a functional expression (like map). This has two benefits:
1. The array can be declared const, and its contents possibly even declared read-only.
2. TypeScript can automatically infer the type/shape of the array contents.
The other example usually cited is that FP will help you understand recursion better, even in non-FP languages.
FP is a paradigm that can be applied at many levels. You can apply it:
- at the code level: removing logical branching in code (https://youtu.be/GyYME8btMHE?t=1473)
- at the high-level design level: event sourcing (https://youtu.be/8JKjvY4etTY)
Although FP languages make it more convenient to write functional-style code, notice the examples above don't need "functional" programming languages. In fact, many "non-functional" languages have borrowed the best parts of functional languages (first-class (lambda) functions, map, reduce, etc)[1].
One of the most valuable, but misunderstood concepts of FP is immutability (functional purity). Grokking Simplicity[2] states it most elegantly: it's not about avoiding mutation because "mutation is bad." In fact, mutation is usually the desired result, but care must be taken because mutation depends on when and how many times it's called.
So good functional programming isn't about avoiding impure functions; instead it's about giving extra care to them. After all, the purpose of all software is to cause some type of mutation/effect (flip pixels on a screen, save bits to storage, send email, etc). Impure functions like these depend on the time they are called, so they are the most difficult to get right.
---
Here's a short list of my top introductions/explanations of FP:
- Grokking Simplicity: https://www.manning.com/books/grokking-simplicity
- Functional Core, Imperative Shell: https://hw.leftium.com/#/item/18043058
- ScottWlaschin: https://hw.leftium.com/#/item/21879368
- Greg Young on Event Sourcing: https://youtu.be/8JKjvY4etTY
- https://project-awesome.org/stoeffel/awesome-fp-js
[0]: https://youtu.be/nuML9SmdbJ4
In your programming education, you may have heard of structured programming, which came about in the 1960s as an attempt to make reasoning about programs more effective. Even if you haven't heard of structured programming before, you have used for and while loops, and you might have been taught about the problems with using GOTO statements for control flow. The fundamental issue with GOTO statements is that it makes it difficult to reason about the program; it can sometimes be difficult to tell which path of execution led to specific states, especially in complex programs where control flow is jumping all over the place and where state is being modified throughout the program. This is why Edgser Dijkstra and other advocates of structured programming discouraged the use of GOTO statements and encouraged a style of programming where control flow is handled by function calls and looping constructs. Some go further, demanding that all control flow constructs have one entry point and one exit point. In practice, this means banning the use of break and continue statements.
Even when removing GOTO statements, there is still the problem of reasoning about how paths of execution in the program led to specific states. The modification of variables that were declared outside of the scope of the block of code modifying the variable can lead to hard-to-debug problems when done incorrectly. This also complicates testing, since it's difficult to test that one block of code as a unit; you need to take into account the variable that is outside the scope of the unit.
Enter functional programming. In pure functional programming, once a variable has been assigned a value, it cannot be modified. While this makes certain stateful computations harder to express, it has the benefit where functions have no side effects, which are modifications of state outside of the scope of the function. By avoiding side effects, the functions can be reasoned about more easily. This also has nice implications for parallel and distributed programming.
In practice, side effects are usually unavoidable since programs still need to perform state modifications such as printing output to the screen, writing to a file, etc. Typically functional programming languages offer escape hatches to handle these tasks. Most functional language programmers encourage a style of programming that separate "pure" and "impure" functions as much as possible, reducing the scope of "impure" functions in order to better reason about the program.
Functional programming can be thought of as the reification of the lambda calculus (https://en.wikipedia.org/wiki/Lambda_calculus), a mathematical expression of computation. There is a heavy emphasis on recursion as opposed to loop constructs, though thanks to tail recursion you don't have to worry about blowing up your stack.
There is a divide between dynamically-typed functional programming and statically-typed functional programming. The dynamically-typed functional programming language world is dominated by Lisp's derivatives, such as Common Lisp, Scheme, Racket, and Clojure. The statically-typed functional programming language world can be split into two categories: (1) those with strict evaluation such as Standard ML, Ocaml, and F#, and (2) those with lazy evaluation such as Miranda and Haskell.
Part of the reason why the functional programming language community has such a strong mathematical bent, especially in the statically-typed world, is because functional programming lends itself well as a vehicle for applying concepts from abstract mathematics to programming. There is a lot of interesting work being done on type systems, theorem-proving systems, formal verification, and other math-influenced work, with languages such as Standard ML and Haskell being the major tools for these researchers.
It can be a tough road getting used to functional programming at first, and some of the literature can be quite intimidating ("a monad is just a monoid in the category of endofunctors, what's the problem?"). However, if you are highly interested in learning about a programming world that encourages reasoning about your programs in an easier (and perhaps even provable) way, then I highly recommend learning about functional programming. Common Lisp/Scheme/Racket/Clojure will show you the beauty of Lisp's high degree of flexibility, while one of the statically-typed functional languages such as Standard ML or Haskell would show you that static types go far beyond what Java and C++ has to offer.