Languages like Scala, Haskell, Clojure, among others, continue to attract programmers every day because of their deep support for functional programming style. Listed below are some core tenents of functional programming:
Functions are the core abstraction, and first class citizens. – Object oriented programming is sufficient for many problems, but is mostly over applied. A functional style which packs more into functions and less into objects has the potential for better modularity and re-usability. Giving functions first class status, allowing them to be passed as arguments to other functions and returned from other functions, gives rise to higher order functions and lets us abstract not just interfaces and contracts for objects, but functional execution structure, transformations of data, cross cutting concerns, and so on.
Values are immutable. – Immutability of values eases the challenge of reasoning about program correctness by taking out one of the most complex pieces of a program: shared mutable state. Programs can instead be defined solely through inputs and outputs of functions. It also eases the way we reason about concurrent access: immutable values, since they cannot be changed, are naturally thread safe. (At this point, that sounds like a silly cop out: “get rid of the synchronization problem by avoiding mutability”… I mean, concurrent programs need to alter shared data, right?) At any rate, ubiquitous mutability, and code that relies on the particular state of objects, gives rise to programs that are a bit more like spaghetti, because there could be very complex interactions happening between many clients that are sharing and mutating the same state. Obviously, some mutable state is necessary (IO in particular!), but it should be used judiciously, not by default.
Lambdas and Closures are powerful tools. – This is really an extension of the tenet regarding functions as the core abstraction. Lambdas are just unnamed functions, and tend to take out lots of boilerplate that would otherwise be required. Closures refer to a runtime behavior that preserves the values within the enclosing scope of where a function has been declared (typically a lambda function). This is a powerful tool for creating functions that are parametrized in some way during creation.
Functions should not have side effects. – This is the main strategy in avoiding mutable state and the pitfalls that go with it. Side effect free functions do not alter any global or shared state, and return output which is predictably derived from its inputs. This is called referential transparency, and is important in program reasoning and execution. If a function call and its output are logically equivalent, the runtime can defer function calls, or automatically execute function calls in parallel (since there’s no way for side effect free functions to rely on global state or even order of execution, as long as one is not an input to another). See also: idempotent.
Declarative over imperative code. – Functional programs tend towards a declarative style instead of an imperative style. That is, instead of telling the computer how to do some set of steps to calculate something, you tell the computer about a set of rules and functions and relationships, and the runtime uses these to compute the final value. Hence, you lean on the runtime to do a lot of “boilerplate” execution, and end up with a program that contains just the declaration of the logic and rules for the program.
Recursion over explicit iteration. – Fallout from previous tenet. A declarative style is likely to favor recursion over iteration when repetition is needed. Iteration isn’t strictly forbidden, but mutable state like a loop counter is. Recursion is typically optimized in a functional language with a technique called tail-recursion, which is enabled by ensuring that a recursive call to a function is the final operation called in the function. Some nice examples of elegant recursive definitions: fibonacci numbers, factorials, quicksort, and many others.
Lazy evaluation is a good thing. – If state is immutable, and functions maintain referential transparency, than we can logically consider a function and set of inputs to be equal to the output of the function. This means a runtime can defer expensive function calls, and only execute them when the output is needed. Also, lazy evaluation is the only way to represent logically infinite data sets and data structures. Lazy evaluation let’s us only compute the needed subset of some infinite data structure (like fetching the first 10 entries from “the set of natural numbers” which is an infinite data set).
Here are a few other points to note about functional programming:
Algebraic Data Types – Algebraic data types are a bit different than the commonly known abstract data types. While the latter says “here’s some interface and contract of behavior, regardless of implementation”, the former is more concerned with defining structure specifically, and doing so in a known finite way. Instead of saying “you can make any subtype of this interface”, it’s saying “this is a type, which may only exist as these other known types”. See this wikipedia article for a decent recap, and also one of its references
Core collection types and functions – Functional programming’s foundation is made up of some basic but powerful building blocks. Core collection types like list and map give us a way to structure data, while core functions like filter, map, and fold give us composable pieces of highly reusable code. In regards to transforming data, these are invaluable. To say these are the fundamental elements of any functional program would be an understatement. They are powerful abstractions that can be composed and reused far more than the specialized types and methods typically created in an object oriented program. Here’s a random online recap of map, fold, and filter. These functions are so composable because they are higher order functions.
Concurrent programming primitives – Functional programming seems to be especially leveraged for concurrent or distributed applications. Though they are not tied directly to the concept of functional programming, the actor model and software transactional memory are important elements of concurrent programming that enjoy native support in many functional languages. The actor model is just the message passing model, and provides a good way to map distributed logic into composable, decoupled, isolated pieces. Software transactional memory leverages native support of atomic “create and swap” operations which conditionally update value references under the condition that the original value matches what is expected. When this doesn’t happen, harmful concurrent access can be detected and handled (usually by the client just trying again). Check out this IBM rundown of Java’s Atomic class