Three key concepts comprise the essence of functional programming: first-order functions, pure functions, and immutable data. Together they express a fundamental architectural paradigm on how units of code interact with each other. While there are numerous languages that specialize in this paradigm, it's gained wide acceptance and can be used in any general purpose language.
Why?
The value in functional programming is in its ability to express transformations of data at a high-level. We don't need to get into the details of how a computer works. A programmer gets to think primarily in the domain of data and calculations.
Functional programming creates a stable system. The code has no ability to corrupt the environment in which it runs. It is predictable and deterministic. Given the constraints on the paradigm, a compiler has significant leeway to optimize and create efficient executables.
Pure functions are essentially the gold standard when it comes to calculations. They are easy to understand, having a single purpose and clear input to output mapping. This makes them easy to test, use and maintain.
This article focuses on the benefits and core qualities of functional programming. As with all paradigms, there are limitations. I'll have to look at those in a future article.
Pure functions
A function is pure if the result is based solely on its input arguments and it doesn't cause any side-effects. Let's look at these two points.
Evaluates from arguments
The result of a pure function is always the same when given the same input arguments. There is no hidden state that could modify the result of the values.
var total = sum( collection )
var C = matrix_mult( A, B )
var safe_name = encode_uri_comp( user_name )
Given a particular collection
the total
will always have the same value. matrix_mult
will behave like the mathemetical definition, giving the single valid result for the multiplication. A standard dictates the transform done by encode_uri_comp
, and it will always be the same.
This is a strong contrast from functions that involve implicit or hidden state.
var value = random( 100 )
var time = get_current_time()
var next = pop( stack )
The result of these functions will not be the same when called again. The result of random
is based on its argument, it will return a value from 0 to 100. It won't, however, return the same value each time, it's relying on a hidden state to return unpredictable values. get_current_time
doesn't even accept arguments yet will produce a new value each time. pop
only depend on its arguments, but since it modifies the stack
it won't return the same value next time.
Side-effect free
A pure function does not create side-effects. This means the state of the system is the same before and after the call. Take a look at a few counter examples, these functions produce side-effects:
print( "Hello" )
stack.push( value )
var handle = open( "file.txt", write )
These change the user's console, an in-memory data structure, or the filesystem. They result in a change in the state of the system thus are not pure functions. Changes in state will alter the results of impure functions.
We only care about observable side-effects. There are several state changes that we ignore for practical reasons. A function can have an internal cache for efficiency. The cache doesn't influence the result, therefore it isn't an observable side-effect. Calculations take time, but we don't consider the change in system time a significant effect. We're just looking at the high-level input/output of functions, and how the user perceives the system.
We ignore these side-effects for the purpose of our definition and compiler implementation. There are of course domains where the impact of memory allocation and time elapsed cannot be ignored.
Immutable data
Related to the pure aspect is the immutable data. This refers to data structures that get a value when created and can never be changed.
var a = point(4,5)
var b = a.normal()
The value of a
will always be 4,5
, it cannot change. normal
creates a new point
as it's return value. The functional paradigm focuses on the transformation of data into new forms without modifying the input data.
There is no difference between assign-by-copy or assign-by-value with immutable data. Since the source value can never be changed it doesn't matter how the assignment is done. This gives the compiler room to optimize, but it also minimizes the thought overhead when reading the code.
Immutables can also improve thread-safety. Once a thread has a reference to an object it no longer needs to worry about synchronization, since that object won't change. This also minimizes ordering concerns in single-threaded code. We don't have to worry about the current state of an object since it has only one state.
Immutable data and side-effect free functions can almost be considered the same feature. You can't effectively use just one of them. They work best together.
First-order functions
Functional programming treats functions as variables just the same as data. In particular, a function can be passed as argument to another function. This is a form of generic programming that helps reduce code redundancy.
var sum = fold( numbers, add )
var product = fold( numbers, mul )
fold
is a common functional concept that inserts an operator between all the items in a list and evaluates the result. Here we pass it add
to perform a summation and mul
to get the product.
var compare = is_reverse ? greater_than | less_than
var abc = sort( names, compare )
This approach binds the function to a named variable for use later in the compare
function.
Lambdas and Closures
Languages with functional features also provide for lambda functions, and closures. Those are technically two different concepts but tend to be exposed within the same feature.
A lambda function is an anonymous function. These are often used as arguments.
var ordered = sort( products, (a,b) -> {
return a.SKU < b.SKU
})
A closure goes a step beyond by allowing the use of variables outside the scope of function. Though often combined with lambdas, they need not be.
var is_reverse = get_config( "product_reverse" )
var compare = (x,y)-> {
return is_ordered ? x.SKU < b.SKU | b.SKU < a.SKU
}
var sorted = sort( products, compare )
In this example, the compare
function binds to the is_ordered
variable defined in the parent scope.
Expressiveness
Functional programming is a valuable tool when implementing algorithms. Pure functions and immutable data make it very appealing in terms of safety as well as compiler optimizations. First order functions introduce an expressive way to reuse concepts and limit code redundancy.
It is often regarded as a pure computational paradigm. It lets us work in the domain of algorithms without being burdened by machine details. This singular focus can improve productivity and clarity.
Wherever the functional paradigm is possible to use, it tends to be the preferred solution. For a complete program, it blends naturally with imperative and reactive programming.
Top comments (2)
So I was wondering why your code blocks weren't pulling in the normal colored formatting and I realized you were writing in Leaf, the language you created. Would love to get some more info from you on Leaf/your process
Ah yes, I often forget that when I cross-post. For my own blog I have a pygments parser that does the formatting. I suppose I could just assign a generic extension here instead, since standard imperative formatting would likely do a decent job of highlighting.
Leaf is my slowly developing program language. I'm trying to use for more and more examples now. Since nobody knows it they pay attention more... at least that's the hope. :)
leaflang.org/ I've been meaning to give that site an overall soon. It's looking dated now.