|
Comments
Did you read today's front page stories & breaking news?
SYS-CON.TV
|
General Java Clarify Your Code in the Functional Style
Clarify Your Code in the Functional Style
Apr. 1, 1999 12:00 AM
In creating the HotScheme interpreter (JDJ Vol. 4, Issue 1), we decided to employ functional programming concepts to Java, our implementation language, whenever it was practical. Functional programming has a number of advantages over more traditional procedural code, which we will enumerate below. The common thread uniting these advantages is an attempt to create code that's conceptually transparent. Employing this functional style directly in Java allowed us to define many Scheme functions in Java the same as they'd be written in Scheme itself. We wouldn't recommend attempting to program Java in a completely functional style -- the resulting Java code would run poorly and appear bizarre and convoluted to most Java programmers. But we do feel that many of the benefits of functional programming can be brought to Java, if some discernment is used about when to apply the style.
What Is Functional Programming? There are six key properties of code written in the functional style, which also hold for mathematical statements in their traditional form. (This list of properties is based on Bruce J. MacLennan's Functional Programming, Addison Wesley, 1990.)
1. Value is independent of the evaluation order.
2. Expressions can be evaluated in parallel.
3. Referential transparency
4. Absence of side effects
5. Inputs to an operation are obvious from the written form.
6. Effects of an operation are obvious from the written form.
Why Employ It? This economy of expression means that algorithms are expressed more directly in the functional style than in procedural code. Because the code isn't busy maintaining state and creating side effects, each expression tends to directly model a step in the algorithm itself. C.S. Peirce pointed out that the form of mathematical expressions is iconic in that the arrangement of terms in an expression is a picture, or icon, of their relationship in our minds (Philosophical Writings of Peirce, Dover, 1940. [reprinted]). Mathematical expressions are the original "visual programming," for these expressions are diagrams of the logical relationship of their terms. So it is with functional programming: the return of a pure function depends only on what you see when looking at its written form, namely, its arguments. The use of its return value is clearly visible from its context, where it will appear as an argument to some other function. In short, a pure function has manifest interfaces. Two examples will illustrate the difference between a pure function and a pseudofunction in this regard. The following is a pure function. The return depends only on the value of the parameter x: int succ (int x) { return x + 1; } The following is a pseudofunction. The return depends on the value of x and the "hidden" value of a: int plusA(int x) { return x + a; } Feeding the function 7 as an argument won't always return the same thing. For you to understand what will happen when plusA() is called, it's not enough to look at the local contexts of the call and the function definition. You must also hunt through the code to where a is defined and then determine where and how its value is set. If the value of a depends on some other global or state variable, then you can soon find yourself reading an entire program in order to understand a single statement. The manifest interfaces of pure functions allow easier proof of correctness. You can imagine how having to follow the state of persistent variables around hundreds of lines of code adds tremendous complexity to a formal proof and pressure for the program to correctly implement its requirements. This can be expressed more formally by saying that the syntax graph and the dataflow graph for a functional language have identical, treelike structures. Subexpressions that communicate data to each other are always found to be adjacent in the syntax tree. This is not true with procedural code as assignments allow nonlocal communication - an assignment in one module of a program can have an effect in an entirely different module when the variable is finally used. Note that encapsulation does not make this any less true; just because you're accessing the variable through a function, rather than directly, doesn't change the degree of nonlocality. The absence of state makes functional programming inherently "thread safe." There are no persistent variables to worry about locking, and therefore no critical code portions that can't be entered simultaneously by different instances of the same function. Since no pure function maintains state or creates side effects, it is, by definition, safe to execute as many of them in parallel as the environment cares to run. Java has many features that reduce bug count significantly. It forces you to have accurate arguments and returns, catches or throws for all thrown exceptions and so forth. The absence of pointers eliminates the source of many bugs in C and C++ programs. There is also runtime trapping (array bounds, etc.), so bugs that slip by the compiler are often found in the first test run of the program rather than lurking silently until a real user hits some peculiar condition and the program blows up. Even so, functional programming offers a whole other level of "bug protection." The lack of assignments (in a pure functional program) removes the need to worry about state, and the possibility of bugs arising from failing to account for all possible pseudofunction states disappears. The inherent thread safety also gets rid of many of the difficulties inherent in debugging threaded programs that are written in a procedural language.
How to Do It in Java Have functional primitives available early on, then use them in higher-level functions designed later. For instance, very early in the project we implemented Length(), first(), second() and restl(), all of which operate on a list argument and return the length, the first item, the second item and a list of all items but the first of that argument, respectively. We also defined a cons() function to build a new list from an object and a list.
Once these functions were done, we used them frequently. See Listing 1 for an example of how we used each of these primitives several times in a fairly small class.
Write: You may find some who claim you are being obscure by writing the second version. However, note that it precisely illustrates the use of all the arguments and subcalls in one statement, while the first version has four times as many lines and three new, unnecessary variables. Use auxiliary functions as another substitute for assignments.
See Listing 2 for an example.
Can we do functional programming in Java and still take advantage of its object-oriented features? We'll examine this question in terms of polymorphism, inheritance, encapsulation and the use of class libraries. Polymorphism is a great aid in writing functional Java. Many of the core functions in the functional style (those for forming and pulling apart lists, for mapping functions over lists and for searching lists) can accept many data types as arguments and may return an object of a different type, depending on what is passed to them. Polymorphism obviates the need to create versions of these functions for all the different permutations of argument and return type. Inheritance doesn't lose any of its applicability in "functional Java." You can still subclass and override methods in subclasses as long as the new methods are pure functions; then you haven't lost any purity of functional style. Encapsulation becomes less important in functional programming since there are fewer state variables and less to encapsulate. However, when you break the functional mold and do employ state variables for performance or code simplicity, encapsulation plays an important role in hiding and localizing the nonfunctional parts of the program. The standard Java library is quite extensive and is partly responsible for the language's success. It and other third-party libraries can be used in one of two ways in a (mostly) functional Java program. The first possibility is to encapsulate them in a functional layer, then use that layer for the rest of the program. The other is simply to accept them as being among the nonfunctional parts of the program wherever they need to be used.
Results and Trade-offs Code may be hard to understand for those unfamiliar with style, a difficulty that is exacerbated by the fact that Java wasn't designed as a functional language. In a language like Scheme, the notation: (print (eval (make term START global_env)))
is easier to decipher than the OOP version: However, we were generally satisfied using the functional style in the HotScheme interpreter. We were able to implement new Scheme functions and even syntactic constructs with remarkably few lines of code, and they were almost always correct the first time we wrote them. Also, to code in this style, we had to think like a Scheme interpreter, so there was a unity of conceptualization between the application and the code that implemented it. Reader Feedback: Page 1 of 1
Latest Cloud Developer Stories
Subscribe to the World's Most Powerful Newsletters
Subscribe to Our Rss Feeds & Get Your SYS-CON News Live!
|
SYS-CON Featured Whitepapers
Most Read This Week
Breaking Cloud Computing News
|
|||||||||||||||||||||||||||||||||||||||||||||||||