Home  Up

A (hopefully) painless introduction to functional programming

by Dan Bensen

This introduction is intended for the nonprogrammer. It avoids technical language as much as possible. As a result, it will sometimes gloss over details that can't easily be explained in nontechnical terms.

Introduction

As its name implies, functional programming relies heavily on the use of functions. Functional programming differs from traditional programming in that functions are treated as objects in the same way that other languages treat variables. One function can be used by another function to customize the second function's behavior.

For example, suppose you have a bunch of numbers, and you want to find their sum and their product. In a functional programming language (FPL), you can do both of these things with one function that will combine the numbers however you tell it to. You need to define a simple function that adds two numbers together, and another one that multiplies them. Then you tell your combiner function to use each of these functions on all your numbers. The combiner function applies your add and multiply functions to the whole list, to compute the overall sum or product.

So the combiner function does both jobs. All the programmer has to do is feed it the add or multiply function, and the combiner function organizes the overall process. The code that does the combining is written only once, even though it's used in two different ways. Plus, you can now use your combiner function to do other things. With any function you define that acts on two numbers, you can now combine a whole set of numbers together without writing any new combiner code. You can even combine non-numerical values by giving the combiner non-numerical functions and non-numerical data.

There are lots of other uses for the combiner function. For instance, you might have a group of true/false values and want to know whether they're all true. To do this, you pass a simple "and" function to your combiner along with the list of values. Without functional programming, this would be significantly harder to do. You would have to either keep rewriting the combiner code in separate functions that are very similar, or use complicated tricks that amount to creating your own FPL. And the program would still have other limitations.

Write once, customize extensively

Most FPLs have the combiner function built into the language one way or another, so you don't have to write it. Also, there are more functions that work like this, which are also included. If you write a small function that does some calculation on one number, or any other single value1, there's a translator, or "do all", function that will take your calculator function and apply it separately to each value in your list. The result is a new list, which is composed of the results generated by your calculator function acting on each value in your original list.

Now suppose you don't need all the values in your list. You want to pick out the good ones and throw away the rest. All you have to do is write a yes/no test function that determines whether or not a value passes your test to be included. There's a filter function that will go through all your values and pick out only the ones that pass the test. The result is a smaller list, with the bad values thrown out.

Finally, every one of these list functions—combine2, translate3, and filter—can deal with other data types. The things inside your list don't have to be numbers. They could be text, arrays, or any other data type. The things in the list could even be lists themselves. All you have to do is write a small function that acts on whatever kind of thing your list contains, and then pass that function to the appropriate list function, depending on what you want to do.

Automatic customization by data type, and programmer-defined customization by functions, drastically reduces the amount of code required to write a large program. In a traditional programming language, this would require a separate function for every different operation you do, and for every data type you want to operate on. There would be more time spent writing almost-identical functions, more room for bugs to creep in, and more work to do any time the program needs to be changed.

Squashing bugs

One of the biggest problems in software development is avoiding and eliminating bugs. The larger a program is, the more places there are where bugs can be introduced. Functional programming helps to deal with this problem in two ways:
  1. Source code is smaller, which reduces the number of places for bugs to hide.
  2. Simpler functions are easier to test and debug.

Smaller source code

A hundred-thousand-line piece of code has ten times as many places bugs can crop up as there are in a ten-thousand-line program. Typically, the longer piece of code will have large amounts of duplication in it, because the language it's written in doesn't support the features required to encapsulate the duplicated code for re-use.

Functional programming languages allow function-based encapsulation that's either not possible or is much more difficult in traditional object-oriented languages.

Simpler functions

Functional programming encourages the use of small, simple functions. Variables are discouraged, because they make functions harder to debug. Without global variables, which are accessible from other parts of the program, a function has only input and output, so it's guaranteed to work for any given output that has been tested. With global variables, the function could still fail if those variables are given different values. Local variables, accessible only within the function, can be eliminated by breaking the function down into several smaller functions that interact by passing values to each other. This way, each smaller function can be tested separately, making it easier to track down which function contains a bug.

They also make it easier for the compiler to generate fast, efficient executable code.

Details

These are some of the methods commonly used in functional programming:
  • recursion
  • functions as first-class objects
  • closures
  • anonymous functions
  • use of function calls instead of variables

Recursion

Recursion is when a function restarts, or "calls", itself, allowing the function to run many times, even though it was only started once. Recursion is similar to the traditional method of looping, where the programmer instructs the computer to do some calculation a specified number of times or until some requirement is satisfied. The difference is that recursion uses function calls where loops use variables.

Although looping and recursion serve a similar purpose (repetition), recursion is ultimately more powerful. Sometimes a function has to repeat itself on several levels, descending into loops inside of loops to do complex operations. But it may not be known beforehand how many levels will be needed.

For instance, suppose your program has to operate on a tree-shaped data structure. A tree is a data object that contains a list of other data objects (branches), each of which has its own sublist of objects, and so on, until the last objects are simply pieces of data (leaves). If the tree is created by the program while it's running, the size and shape of the tree can't be determined beforehand.

In this case, looping doesn't work very well. The programmer using loops has to tell the computer from the beginning how many levels of branches will exist. But the number of loops isn't known until the tree is created.

With recursion, it's much easier. You just write your function to deal with one layer of branches, and then test to see if the next layer has more branches. If it does, you have your function call itself, which creates another copy of the function to process the next layer. New copies of the function are created until the tree is completely processed. In a sense, this is equivalent to the program writing a new loop at each layer, rather than having to specify the loops ahead of time.

Functions as first-class objects

A first-class object is something that can be created or defined in one part of a program, and then passed to another part of the program to be used. Traditional languages do this with constants, variables, and data objects, but not with functions. Passing a function to another function allows you to customize the second function, as we did with the list functions. In a functional programming language, if several functions are structurally identical in some way, but have different functionality somewhere inside, then they can be written together as one function that recieves other functions to specify that internal functionality. The smaller amount of source code makes debugging easier.

Closures

A closure is a function that's defined inside another function, but used somewhere else. The closure uses internal variables that are not accessible outside the defining function. In a traditional programming language, these internal variables would disappear once the defining function has completed its work. With closures, those variables are retained. The defining function passes the closure to other functions for them to use, complete with the internal variables. This allows the creation of custom functions to allow even more versatile customization.

Anonymous functions

For historical reasons, anonymous functions are also called "lambda" functions. An anonymous function is a small function that's generated on the spot in the middle of a calculation, often to be used immediately. It's a convenient way of defining a function without the programmer having to think of a name for it if it's only going to be used in one place. Other than not having a name, an anonymous function is just a function like any other. Sometimes a closure is created as an anonymous function. When another part of the program recieves the closure, it may be given whatever name the programmer using it chooses.

Avoidance of variables

In functional programming, a large function and its internal variables are replaced with a set of smaller interacting functions. In addition to easier testing and debugging, there's another benefit. High-performance programs are compiled in order to maximize their speed. When a program is compiled, a good compiler will optimize the program as much as possible. But how much optimization the compiler can do depends on how the program is written. Variables make a program hard to optimize. Without variables, certain properties of the program can be guaranteed ahead of time, so the compiler can make a functional program very fast. This is generally not possible when variables are used.

Other tools

Some other methods used in functional programming are
  • recursive data structures
  • everything is an expression
  • currying of functions5
  • type inference, pattern matching
All of these methods help to reduce the amount of code programmers have to write in order to create a piece of software. As projects get larger, the size of the source code becomes critical, making these tools indispensible in keeping large projects manageable.

Functional programming in non-FP languages

FPLs are custom-made for functional programming, and as a result, they naturally implement functional programming very well. However, some FP techniques can (and should) be used in other languages. Small functions, avoidance of variables, and simulated function objects can all make a program smaller and easier to work on.

Non-FPLs use various tricks4 to fake first-class functions. These methods can be very helpful, and should be used. But they lack the ability of closures to be customized by the internal variables that were available when the closure was defined. They also don't support a functional technique5 that allows a general, all-purpose function to easily define more specific functions.

Conclusion

Anything that reduces the amount of source code required to write a program is beneficial. Object-oriented programming has served to do some of this, but its limits are becoming more obvious every year. Popular design patterns are used in object-oriented languages because object-oriented languages don't support the functional-programming methods implemented by those patterns. Functional programming languages do.

Is anything in this introduction confusing? If you have questions, suggestions, complaints, or any other comments, please feel free to let me know.

1 Actually, even the values in a list can be lists or other groups of values. In that case, you would pass one list function to another list function, saving yourself twice as much work.
2 The combiner function is usually called something like "reduce" or "fold".
3 The translator is called "map" or something similar.
4 C uses function pointers, C++ can use pointers or function classes, Java uses interface (inner) classes, and C# uses delegates.
5 Currying. This technique allows the programmer to combine a function with one or more of its parameters to create a new, more specific function. For instance, a function that adds two numbers could be given the value 1 as its first parameter to define a function that increments a number (the other parameter) by 1. The new function takes only one parameter, because the other one is already built into it.

© 2006 Dan Bensen   Home   About me