Preface
Over time, languages and runtimes have handled more and more mundane details for us. Functional languages exemplify this trend, but modern dynamic languages have also incorporated many functional features to make developers' lives easier. This installment investigates some of the functional features already lurking in Groovy, showing how recursion hides state and how to build lazy lists.
Confession: I never want to work in a non-garbage-collected language again. I paid my dues in languages like C++ for too many years, and I don't want to surrender the conveniences of modern languages. That's the story of how software development progresses. We build layers of abstraction to handle (and hide) mundane details. As the capabilities of computers have grown, we've offloaded more tasks to languages and runtimes. As recently as a decade ago, developers shunned interpreted languages for being too slow for production applications, but they are common now. Many of the features of functional languages were prohibitively slow a decade ago but make perfect sense now because they optimize developer time and effort.
Many of the features I cover in this article series show how functional languages and frameworks handle mundane details. However, you don't have to go to a functional language to start reaping benefits from functional constructs. In this installment and the next, I'll show how some functional programming has already been applied into Groovy.
Groovy's functional-ish lists
Groovy significantly augments the Java collection libraries, including adding functional constructs. The first favor Groovy does for you is provide a different perspective on lists, which seems trivial at first but offers some interesting benefits.
Seeing lists differently
If your background is primarily in C or C-like languages (including Java), you probably conceptualize lists as indexed collections. This perspective makes it easy to iterate over a collection, even when you don't explicitly use the index, as shown in the Groovy code in Listing 1:
- Listing 1. List traversal using (hidden) indexes
- def perfectNumbers = [6, 28, 496, 8128]
- def iterateList(listOfNums) {
- listOfNums.each { n ->
- println "${n}"
- }
- }
- iterateList(perfectNumbers)
Figure 1. Lists as indexed slots
Many functional languages have a slightly different perspective on lists, and fortunately Groovy shares this perspective. Instead of thinking of a list as indexed slots, think of it as a combination of the first element in the list (the head) plus the remainder of the list (the tail), as shown in Figure 2:
Figure 2. A list as its head and tail
Thinking about a list as head and tail allows me to iterate through it using recursion, as shown in Listing 2:
- Listing 2. List traversal using recursion
- def recurseList(listOfNums) {
- if (listOfNums.size == 0) return;
- println "${listOfNums.head()}"
- recurseList(listOfNums.tail())
- }
- recurseList(perfectNumbers)
Recursion has technical limits built into the platform (see Resources), so this isn't a panacea. But it should be safe for lists that contain a small number of items. I'm more interested in investigating the impact on the structure of the code, in anticipation of the day when the limits ease or disappear. Given the shortcomings, the benefit of the recursive version may not be immediately obvious. To make it more so, consider the problem of filtering a list. In Listing 3, I show an example of a filtering method that accepts a list and a predicate (a boolean test) to determine if the item belongs in the list:
- Listing 3. Imperative filtering with Groovy
- def filter(list, p) {
- def new_list = []
- list.each { i ->
- if (p(i))
- new_list << i
- }
- new_list
- }
- modBy2 = { n -> n % 2 == 0}
- evenNums = filter(1..20, modBy2)
- printf("Even number between 1~20: ${evenNums}\n")
Consider a recursive implementation of the filter method from Listing 3, shown in Listing 4:
- Listing 4. Recursive filtering with Groovy
- def recFilter(list, p) {
- if (list.size() == 0) return list
- if (p(list.head()))
- []+ list.head() + filter(list.tail(), p)
- else
- filter(list.tail(), p)
- }
- evenNums = recFilter(1..20, {n-> n % 2 == 0})
- printf("Even number between 1~20: ${evenNums}\n")
The difference between Listing 3 and Listing 4 highlights an important question: Who's minding the state? In the imperative version, I am. I must create a new variable named new_list, I must add things to it, and I must return it when I'm done. In the recursive version, the language manages the return value, building it up on the stack as the recursive return for each method invocation. Notice that every exit route of the recFilter() method in Listing 4 is a return call, which builds up the intermediate value on the stack.
Although not as dramatic a life improvement as garbage collection, this does illustrate an important trend in programming languages: offloading moving parts. If I'm never allowed to touch the intermediate results of the list, I cannot introduce bugs in the way that I interact with it. This perspective shift about lists allows you explore other aspects, such as a list's size and scope.
Lazy lists in Groovy
One of the common features of functional languages is the lazy list: a list whose contents are generated only as you need it. Lazy lists allow you to defer initialization of expensive resources until you absolutely need them. They also allow the creation of infinite sequences: lists that have no upper bound. If you aren't required to say up front how big the list could be, you can let it be as big as it needs to be.
First, I'll show you an example of using a lazy list in Groovy in Listing 5, and then I'll show you the implementation:
- Listing 5. Using lazy lists in Groovy
- def prepend(val, closure) { new LazyList(val, closure) }
- def integers(n) { prepend(n, { integers(n + 1) }) }
- def naturalNumbers = integers(1)
- printf("Top 10 number: %s\n", naturalNumbers.getHead(10).join(' '))
- def evenNumbers = naturalNumbers.filter { it % 2 == 0 }
- printf("Top 10 even number: %s\n", evenNumbers.getHead(10).join(' '))
To retrieve values from the list, I call the getHead() method, which returns the argument number of values from the top of the list. In Listing 5, naturalNumbers is a lazy sequence of all integers. To get some of them, I call the getHead() method, specifying how many integers I want. Call naturalNumbers.getHead(10) to receive a list of the first 10 natural numbers. Using the filter() method, I retrieve a lazy list of even numbers and call the getHead() method to fetch the first 10 even numbers.
The implementation of LazyList appears in Listing 6:
- Listing 6. LazyList implementation
- class LazyList {
- private head, tail
- LazyList(head, tail) {
- this.head = head;
- this.tail = tail
- }
- def LazyList getTail() { tail ? tail() : null }
- def List getHead(n) {
- def valuesFromHead = [];
- def current = this
- n.times {
- valuesFromHead << current.head
- current = current.tail
- }
- valuesFromHead
- }
- def LazyList filter(Closure p) {
- if (p(head))
- p.owner.prepend(head, { getTail().filter(p) })
- else
- getTail().filter(p)
- }
- }
The filter() method in Listing 5 uses the same recursive approach as Listing 4 but implements it as part of the list rather than a stand-alone function. Lazy lists exist in Java (see Resources) but are much easier to implement in languages that have functional features. Lazy lists work great in situations in which generating resources are expensive, such as getting lists of perfect numbers.
Lazy list of perfect numbers
If you've been following this article series, you're well aware of my favorite guinea-pig code, finding perfect numbers (see "Thinking functionally, Part 1"). One of the shortcomings of all the implementations so far is the need to specify the number for classification. Instead, I want a version that returns a lazy list of perfect numbers. Toward that goal, I've written a highly functional, very compact perfect-number finder that supports lazy lists, shown in Listing 7:
- Listing 7. Pared-down version of number classifier, including nextPerfectNumberFrom() method
- class NumberClassifier {
- static def factorsOf(number) {
- (1..number).findAll { i -> number % i == 0 }
- }
- static def isPerfect(number) {
- factorsOf(number).inject(0, {i, j -> i + j}) == 2 * number
- }
- static def nextPerfectNumberFrom(n) {
- while (! isPerfect(++n)) ;
- n
- }
- }
Using this new version of NumberClassifier, I can create a lazy list of perfect numbers, as shown in Listing 8:
- Listing 8. Lazily initialized list of perfect numbers
- def perfectNumbers(n) {
- prepend(n, { perfectNumbers(nextPerfectNumberFrom(n)) })
- };
- @Test
- public void infinite_perfect_number_sequence() {
- def perfectNumbers = perfectNumbers(nextPerfectNumberFrom(1))
- assertEquals([6, 28, 496], perfectNumbers.getHead(3))
- }
Calculating new perfect numbers is expensive, so I would rather do it as little as possible. By building it as a lazy list, I can defer calculations until the optimum time. It is more difficult to think about infinite sequences if your abstraction of "list" is "numbered slots." Thinking of a list as the "first element" and the "rest of the list" encourages you to think of the elements in the list rather than the structure, which in turn allows you to think about things like lazy lists.
Conclusion
One of the ways that developers make quantum leaps in productivity is by building effective abstractions to hide details. We would never get anywhere if we were still coding with ones and zeros. One of the appealing aspects of functional languages is the attempt to abstract more details away from developers.Modern dynamic languages on the JVM already give you many of these features. In this installment, I showed how just shifting your perspective a little bit on how lists are constructed allows the language to manage state during iteration. Also, when you think of lists as "head" and "tail," it allows you to build things like lazy lists and infinite sequences.
Supplement
* The Productive Programmer (Neal Ford, O'Reilly Media, 2008): Neal Ford's most recent book discusses tools and practices that help you improve your coding efficiency.
* An Excursion in Java Recursion: Recursion in Java has well-known limits, including some pointed out in this blog.
* Find limit of recursion: Find the recursion depth on your platform with these tests from RosettaCode.
* Apache Commons LazyList: Apache Commons includes a lazy-list implementation.
* Browse the technology bookstore for books on these and other technical topics.
* developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
沒有留言:
張貼留言