Functional Programming Concepts

Functional programming leads to code that is easier to write, read, test, and reuse. Here’s how it works.

Pure Functions

Programs are built out of pure functions. A pure function has no side effects; that is, it doesn’t depend on anything but its arguments, and its only influence on the outside world is through its return value.

Mathematical functions are pure functions. Two plus two is four, no matter where or when you ask. Also, asking doesn’t do anything other than return the answer.

Program output is decidedly impure. For example, when you println, you change the outside world by pushing data onto an output stream. Also, the results of println depend on state outside the function: the standard output stream might be redirected, closed, or broken.

If you start writing pure functions, you’ll quickly realize that pure functions and immutable data go hand in hand. Consider the following mystery function:

 (​defn​ mystery [input]
  (​if​ input data-1 data-2))

If mystery is a pure function, then regardless of what it does, data-1 and data-2 have to be immutable! Otherwise, changes to the data would cause the function to return different values for the same input.

A single piece of mutable data can ruin the game, rendering an entire call chain of functions impure. So, once you make a commitment to writing pure functions, you end up using immutable data in large sections of your application.

Persistent Data Structures

Immutable data is critical to Clojure’s approach to both FP and state. On the FP side, pure functions cannot have side effects, such as updating the state of a mutable object. On the state side, Clojure’s reference types require immutable data structures to implement their concurrency guarantees.

The fly in the ointment is performance. When all data is immutable, “update” translates into “create a copy of the original data, plus my changes.” This will use up memory quickly! Imagine that you have an address book that takes up 5 MB of memory. Then, you make five small updates. With a mutable address book, you are still consuming about 5 MB of memory. But if you have to copy the whole address book for each update, then an immutable version would balloon to 25 MB!

Clojure’s data structures don’t take this naive “copy everything” approach. Instead, all Clojure data structures are persistent. In this context, persistent means that the data structures preserve old copies of themselves by efficiently sharing structure between older and newer versions.

Structural sharing is easiest to visualize with a list. Consider list a with two elements:

 (​def​ a '(1 2))
 -> #​'user/a

Then from a, you can create a b with an additional element added:

 (​def​ b (cons 0 a))
 -> #​'user/b

b can reuse all of a’s structure, rather than have its own private copy:

images/shared-structure.png

All of Clojure’s data structures share structure where possible. For structures other than simple lists, the mechanics are more complex, of course. If you’re interested in the details, check out the following articles:

  • “Ideal Hash Trees”[22] by Phil Bagwell
  • “Understanding Clojure’s PersistentVector Implementation”[23] by Karl Krukow

Laziness and Recursion

Functional programs make heavy use of recursion and laziness. A recursion occurs when a function calls itself, either directly or indirectly. With laziness, an expression’s evaluation is postponed until it’s actually needed. Evaluating a lazy expression is called realizing the expression.

In Clojure, functions and expressions are not lazy. However, sequences are generally lazy. Because so much Clojure programming is sequence manipulation, you get many of the benefits of a fully lazy language. In particular, you can build complex expressions using lazy sequences and then “pay” only for the elements you actually need.

Lazy techniques imply pure functions. You never have to worry about when to call a pure function, since it always returns the same thing. Impure functions, on the other hand, do not play well with lazy techniques. As a programmer, you must explicitly control when an impure function is called, because if you call it at some other time, it may behave differently!

Referential Transparency

Laziness depends on the ability to replace a function call with its result at any time. Functions that have this ability are called referentially transparent, because calls to such functions can be replaced without affecting the behavior of the program. In addition to laziness, referentially transparent functions can also benefit from the following:

  • Memoization, automatic caching of results
  • Automatic parallelization, moving function evaluation to another processor or machine

Pure functions are referentially transparent by definition. Most other functions are not referentially transparent, and those that are must be proven safe by code review.

Benefits of FP

Well, that is a lot of terminology, and we promised it would make your code easier to write, read, test, and compose. Here’s how.

You’ll find functional code easier to write because the relevant information is right in front of you, in a function’s argument list. You don’t have to worry about global scope, session scope, application scope, or thread scope. Functional code is easier to read for exactly the same reason.

Code that is easier to read and write is going to be easier to test, but functional code brings an additional benefit for testing. As projects get large, it often takes a lot of effort to set up the right environment to execute a test. This is much less of a problem with functional code, because there is no relevant environment beyond the function’s arguments.

Functional code improves reuse. To reuse code, you must be able to do the following:

  • Find and understand a piece of useful code.
  • Compose the reusable code with other code.

The readability of functional code helps you find and understand the functions you need, but the benefit for composing code is even more compelling.

Composability is a hard problem. For years, programmers have used encapsulation to try to create composable code. Encapsulation creates a firewall, providing access to data only through a public API.

Encapsulation helps, but it’s nowhere near enough. Even with encapsulated objects, there are far too many surprising interactions when you try to compose entire systems. The problem is those darn side effects. Impure functions violate encapsulation, because they let the outside world reach in (invisibly!) and change the behavior of your code. Pure functions, on the other hand, are truly encapsulated and composable. Put them anywhere you want in a system, and they will always behave in the same way.

Guidelines for Use

Clojure takes a unique approach to FP that strikes a balance between academic purity and the reality of running well on the JVM. That means there’s a lot to learn all at once. But fear not. If you’re new to FP, the following guidelines will help on your initial steps toward FP mastery, Clojure-style:

  1. Avoid direct recursion. The JVM can’t optimize recursive calls, and Clojure programs that recurse will blow their stack.

  2. Use recur when you’re producing scalar values or small, fixed sequences. Clojure will optimize calls that use an explicit recur.

  3. When producing large or variable-sized sequences, always be lazy. (Do not recur.) Then, your callers can consume just the part of the sequence they actually need.

  4. Be careful not to realize more of a lazy sequence than you need.

  5. Know the sequence library. You can often write code without using recur or the lazy APIs at all.

  6. Subdivide. Divide even simple-seeming problems into smaller pieces, and you’ll often find solutions in the sequence library that lead to more general, reusable code.

The last two guidelines are particularly important. If you’re new to FP, you can translate those to: “Ignore this chapter and just use the techniques in Chapter 3, Unifying Data with Sequences until you hit a wall.”

Now, let’s get started writing functional code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.97.126