Preface
This chapter covers: (Functional Programming in Java)
Not everybody agrees on a definition for functional programming (FP). In general terms, functional programming is a programming paradigm, and it’s about programming with functions. But this doesn’t explain the most important aspect: how FP is different from other paradigms, and what makes it a (potentially) better way to write programs. In his article “Why Functional Programming Matters,” published in 1990, John Hughes writes the following:
John Hughes, “Why Functional Programming Matters,” from D. Turner, ed., Research Topics in Functional Programming (Addison-Wesley, 1990), 17–42, www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf.
In the rest of this chapter, I’ll briefly present concepts such as referential transparency and the substitution model, as well as other concepts that together are the essence of functional programming. You’ll apply these concepts over and over in the coming chapters.
What is functional programming?
It’s often as important to understand what something is not, as to agree about what it is. If functional programming is a programming paradigm, there clearly must be other programming paradigms that FP differs from. Contrary to what some might think, functional programming isn’t the opposite of object-oriented programming (OOP). Some functional programming languages are object-oriented; some are not. Functional programming is sometimes considered to be a set of techniques that supplement or replace techniques found in other programming paradigms, such as:
Although it is true that most functional languages do use a number of these techniques, you may find, for each of them, examples of functional programming languages that don’t, as well as non-functional languages that do. As you’ll see when studying each of these techniques in this book, it’s not the language that makes programming functional. It’s the way you write the code. But some languages are more functional-friendly than others.
What functional programming may be opposed to is the imperative programming paradigm. In imperative programming style, programs are composed from elements that “do” something. “Doing” something generally implies an initial state, a transition, and an end state. This is sometimes called state mutation. Traditional imperative-style programs are often described as a series of mutations, separated with condition testing. For example, an addition program for adding two positive values a and b might be represented by the following pseudo code:
In this pseudo code, you can recognize the traditional instructions of most imperative languages: testing conditions, mutating variables, branching, and returning a value. This code may be represented graphically by a flow chart, such as figure 1.1.
Figure 1.1. A flow chart representing an imperative program as a process that occurs in time. Various things are transformed and states are mutated until the result is obtained.
On the other hand, functional programs are composed of elements that “are” something—they don’t “do” something. The addition of a and b doesn’t “make” a result. The addition of 2 and 3, for example, doesn’t make 5. It is 5. The difference might not seem important, but it is. The main consequence is that each time you encounter 2 + 3, you can replace it with 5. Can you do the same thing in an imperative program? Well, sometimes you can. But sometimes you can’t without changing the program’s outcome. If the expression you want to replace has no other effect than returning the result, you can safely replace it with its result. But how can you be sure that it has no other effect? In the addition example, you clearly see that the two variables a and b have been destroyed by the program. This is an effect of the program, besides returning the result, so it’s called a side effect. (This would be different if the computation were occurring inside a Java method, because the variables a and b would be passed by value, and the change would then be local and not visible from outside the method.)
One major difference between imperative programming and FP is that in FP there are no side effects. This means, among other things,
When I say “no side effects,” I mean no observable side effects. Functional programs are built by composing functions that take an argument and return a value, and that’s it. You don’t care about what’s happening inside the functions, because, in theory, nothing is happening ever. But in practice, programs are written for computers that aren’t functional at all. All computers are based on the same imperative paradigm; so functions are black boxes that
This is theory. In practice, it’s impossible for a function to have no side effects at all. A function will return a value at some time, and this time may vary. This is a side effect. It might create an out-of-memory error, or a stack-overflow error, and crash the application, which is a somewhat observable side effect. And it will cause writing to memory, registering mutations, thread launching, context switching, and other sorts of things that are indeed effects observable from outside.
So functional programming is writing programs with no intentional side effects, by which I mean side effects that are part of the expected outcome of the program. There should also be as few non-intentional side effects as possible.
Writing useful programs with no side effects
You may wonder how you can possibly write useful programs if they have no side effects. Obviously, you can’t. Functional programming is not about writing programs that have no observable results. It’s about writing programs that have no observable results other than returning a value. But if this is all the program does, it won’t be very useful. In the end, functional programs have to have an observable effect, such as displaying the result on a screen, writing it to a file or database, or sending it over a network. This interaction with the outside world won’t occur in the middle of a computation, but only when you finish the computation. In other words, side effects will be delayed and applied separately.
Take the example of the addition in figure 1.1. Although it’s described in imperative style, it might yet be functional, depending on how it’s implemented. Imagine this program is implemented in Java as follows:
- public static int add(int a, int b) {
- while (b > 0) {
- a++;
- b--;
- }
- return a;
- }
- public static int div(int a, int b) {
- return a / b;
- }
- public static int div(int a, int b) {
- return (int) (a / (float) b);
- }
- public static void add(int a, int b) {
- while (b > 0) {
- a++;
- b--;
- }
- System.out.println(a);
- }
- public static int add(int a, int b) {
- log(String.format("Adding %s and %s", a, b));
- while (b > 0) {
- a++;
- b--;
- }
- log(String.format("Returning %s", a));
- return a;
- }
How referential transparency makes programs safer
Having no side effects (and thus not mutating anything in the external world) isn’t enough for a program to be functional. Functional programs must also not be affected by the external world. In other words, the output of a functional program must depend only on its argument. This means functional code may not read data from the console, a file, a remote URL, a database, or even from the system. Code that doesn’t mutate or depend on the external world is said to be referentially transparent. Referentially transparent code has several properties that might be of some interest to programmers:
Figure 1.2 illustrates the difference between a referentially transparent program and one that’s not referentially transparent.
Figure 1.2. Comparing a program that’s referentially transparent to one that’s not
The benefits of functional programming
From what I’ve just said, you can likely guess the many benefits of functional programming:
Functional programs are inherently thread-safe because they avoid mutation of shared state. Once again, this doesn’t mean that all data has to be immutable. Only shared data must be. But functional programmers will soon realize that immutable data is always safer, even if the mutation is not visible externally.
Using the substitution model to reason about programs
Remember that a function doesn’t do anything. It only has a value, which is only dependent on its argument. As a consequence, it’s always possible to replace a function call, or any referentially transparent expression, with its value, as shown in figure 1.3.
Figure 1.3. Replacing referentially transparent expressions with their values doesn’t change the overall meaning.
When applied to functions, the substitution model allows you to replace any function call with its return value. Consider the following code:
- public static void main(String[] args) {
- int x = add(mult(2, 3), mult(4, 5));
- }
- public static int add(int a, int b) {
- log(String.format("Returning %s as the result of %s + %s", a + b, a, b));
- return a + b;
- }
- public static int mult(int a, int b) {
- return a * b;
- }
- public static void log(String m) {
- System.out.println(m);
- }
- int x = add(6, 20);
Applying functional principles to a simple example
As an example of converting an imperative program into a functional one, we’ll consider a very simple program representing the purchase of a donut with a credit card.
In this code, the charging of the credit card is a side effect (1). Charging a credit card probably consists of calling the bank, verifying that the credit card is valid and authorized, and registering the transaction. The function returns the donut (2). The problem with this kind of code is that it’s difficult to test. Running the program for testing would involve contacting the bank and registering the transaction using some sort of mock account. Or you’d need to create a mock credit card to register the effect of calling the charge method and to verify the state of the mock after the test.
If you want to be able to test your program without contacting the bank or using a mock, you should remove the side effect. Because you still want to charge the credit card, the only solution is to add a representation of this operation to the return value. Your buyDonut method will have to return both the donut and this representation of the payment. To represent the payment, you can use a Payment class.
- Listing 1.2. The Payment class
- public class Payment {
- public final CreditCard creditCard;
- public final int amount;
- public Payment(CreditCard creditCard, int amount) {
- this.creditCard = creditCard;
- this.amount = amount;
- }
- }
- public class Purchase {
- public Donut donut;
- public Payment payment;
- public Purchase(Donut donut, Payment payment) {
- this.donut = donut;
- this.payment = payment;
- }
- }
- Listing 1.3. The Tuple class
- public class Tuple
{ - public final T _1;
- public final U _2;
- public Tuple(T t, U u) {
- this._1 = t;
- this._2 = u;
- }
- }
- public class DonutShop {
- public static Tuple
buyDonut(CreditCard creditCard) { - Donut donut = new Donut();
- Payment payment = new Payment(creditCard, Donut.price);
- return new Tuple<>(donut, payment);
- }
- }
The combine method in the following listing allows you to combine payments. Note that if the credit cards don’t match, an exception is thrown. This doesn’t contradict what I said about functional programs not throwing exceptions. Here, trying to combine two payments with two different credit cards is considered a bug, so it should crash the application. (This isn’t very realistic. You’ll have to wait until chapter 7 to learn how to deal with such situations without throwing exceptions.)
- Listing 1.4. Composing multiple payments into a single one
- package com.fpinjava.introduction.listing01_04;
- public class Payment {
- public final CreditCard creditCard;
- public final int amount;
- public Payment(CreditCard creditCard, int amount) {
- this.creditCard = creditCard;
- this.amount = amount;
- }
- public Payment combine(Payment payment) {
- if (creditCard.equals(payment.creditCard)) {
- return new Payment(creditCard, amount + payment.amount);
- } else {
- throw new IllegalStateException("Cards don't match.");
- }
- }
- }
- , Payment>.
- Listing 1.5. Buying multiple donuts at once
- package com.fpinjava.introduction.listing01_05;
- import static com.fpinjava.common.List.fill;
- import com.fpinjava.common.List;
- import com.fpinjava.common.Tuple;
- public class DonutShop {
- public static Tuple
buyDonut( final CreditCard cCard) { - return new Tuple<>(new Donut(), new Payment(cCard, Donut.price));
- }
- public static Tuple
- , Payment> buyDonuts(
- final CreditCard cCard) {
- return new Tuple<>(fill(quantity, () -> new Donut()),
- new Payment(cCard, Donut.price * quantity));
- }
- }
- public static Tuple
- , Payment> buyDonuts(
- final CreditCard cCard) {
- return new Tuple<>(Collections.nCopies(quantity, new Donut()),
- new Payment(cCard, Donut.price * quantity));
- }
Now, your program can be tested without using a mock. For example, here’s a test for the method buyDonuts:
- @Test
- public void testBuyDonuts() {
- CreditCard creditCard = new CreditCard();
- Tuple
- , Payment> purchase = DonutShop.buyDonuts(5, creditCard);
- assertEquals(Donut.price * 5, purchase._2.amount);
- assertEquals(creditCard, purchase._2.creditCard);
- }
- public Map> groupBy(Function f)
- groupBy Ex1:
- package fp.groupby;
- import java.util.Arrays;
- import java.util.List;
- import java.util.Map;
- import java.util.function.Function;
- import java.util.stream.Collectors;
- public class Ex1 {
- /**
- * @see
- * https://stackoverflow.com/questions/33345413/java-8-grouping-using-custom-collector
- * https://stackoverflow.com/questions/32272770/java-8-groupingby-with-custom-key
- * @param args
- */
- public static void main(String[] args) {
- //3 apple, 2 banana, others 1
- List
items = - Arrays.asList("apple", "apple", "banana", "beetroot", "olive", "pepper",
- "apple", "orange", "banana", "papaya", "apricot", "oranges");
- Map
> rstBy = items.stream().collect(Collectors.groupingBy(Function.identity())); - System.out.printf("Result1:\n%s\n\n", rstBy);
- Map
> rstByFC = items.stream().collect(Collectors.groupingBy(Ex1::ByFC)); - System.out.printf("Result2:\n%s\n\n", rstByFC);
- Map
rstByCnt = - items.stream().collect(
- Collectors.groupingBy(
- Function.identity(), Collectors.counting()
- )
- );
- System.out.printf("Result3:\n%s\n", rstByCnt);
- }
- public static String ByFC(String str)
- {
- return String.valueOf(str.charAt(0));
- }
- }
Now let's take a look at another useful function used frequently in FP:
- List map(Function f)
- package fp.map;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- import java.util.stream.Collectors;
- import static java.util.stream.Collectors.toList;
- /**
- * http://www.java67.com/2015/01/java-8-map-function-examples.html
- * @author E7450
- *
- */
- public class Java8MapExample {
- public static void main(String[] args) {
- List
cities = Arrays.asList("London", "HongKong", "Paris", "NewYork"); - System.out.println("Original list : " + cities);
- System.out.println("list transformed using Java 8 :" + transform(cities));
- System.out.println("list transformed using loop before Java 8 : " + beforeJava8(cities));
- // You can even on the fly tranform Collection in Java using Map function
- // let's transform a List of integers to square each element
- List
numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); - List
squares = numbers.stream().map( i -> i*i).collect(Collectors.toList()); - System.out.println("original list of numbers : " + numbers);
- System.out.println("transformed list of integers using Map in Java 8 : " + squares);
- }
- /**
- * This is how you convert all elements of list into upper case using loop before Java 8
- * @param listOfString
- * @return List with each element converted into upper case */
- public static List
beforeJava8(List listOfString) { - List
coll = new ArrayList<>(); - for (String str : listOfString) {
- coll.add(str.toUpperCase());
- }
- return coll;
- }
- /**
- * You can use Java 8 map function to transform each element of list
- * @param listOfString
- * @return list of elements with upper case */
- public static List
transform(List listOfString) { - return listOfString.stream() // Convert list to Stream
- .map(String::toUpperCase) // Convert each element to upper case
- .collect(toList()); // Collect results into a new list
- }
- }
- Tuple
- , List
> unzip(Function> f)
- A reduce(Function> f)
This method of List uses an operation to reduce the list to a single value. This operation is represented by Function> f. This notation may look a bit weird, but you’ll learn what it means in chapter 2. It could be, for example, an addition. In such a case, it would simply mean a function such as f(a, b) = a + b. A simple usage example:
- package fp.reduce;
- import java.util.Arrays;
- /**
- * @see
- * https://www.sitepoint.com/java-8-streams-filter-map-reduce/
- * @author E7450
- *
- */
- public class Ex1 {
- public static void main(String[] args) {
- String[] myArray = { "this", "is", "a", "sentence" };
- System.out.printf("Result='%s'\n", Arrays.stream(myArray).reduce("", (a, b) -> a + b));
- }
- }
Using these methods, you can now create a new method that groups payments by credit card.
Note that you could use a method reference in the last line of the groupByCard method, but I chose the lambda notation because it’s probably (much) easier to read. If you prefer method references, you can replace this line with the following one:
- .map(x -> x.reduce(c1 -> c1::combine));
In listing 1.6, the portion after c1 -> is a function taking a single parameter and passing that parameter to c1.combine(). And that’s exactly what c1::combine is—it’s a function taking a single parameter. Method references are often easier to read than lambdas, but not always!
Pushing abstraction to the limit
As you’ve seen, functional programming consists in writing programs by composing pure functions, which means functions without side effects. These functions may be represented by methods, or they may be first-class functions, such as the arguments of methods groupBy, map, or reduce, in the previous example. First-class functions are simply functions represented in such a way that, unlike methods, they can be manipulated by the program. In most cases, they’re used as arguments to other functions, or to methods. You’ll learn in chapter 2 how this is done.
But the most important notion here is abstraction. Look at the reduce method. It takes as its argument an operation, and uses it to reduce a list to a single value. Here, the operation has two operands of the same type. Except for this, it could be any operation. Consider a list of integers. You could write a sum method to compute the sum of the elements; you could write a product method to compute the product of the elements; or you could write a min or a max method to compute the minimum or the maximum of the list. But you could also use the reduce method for all these computations. This is abstraction. You abstract the part that is common to all operations in the reduce method, and you pass the variable part (the operation) as an argument.
But you could go further. The reduce method is a particular case of a more general method that might produce a result of a different type than the elements of the list. For example, it could be applied to a list of characters to produce a String. You’d need to start from a given value (probably an empty string). In chapters 3 and 5, you’ll learn how to develop this method (called fold). Also note that the reduce method won’t work on an empty list. Think of a list of integers—if you want to compute the sum, you need to have an element to start with. If the list is empty, what should you return? Of course, you know that the result should be 0, but this only works for a sum. It doesn’t work for a product.
Also consider the groupByCard method. It looks like a business method that can only be used to group payments by credit cards. But it’s not! You could use this method to group the elements of any list by any of their properties, so this method should be abstracted and put inside the List class in such a way that it could be reused easily.
A very important part of functional programming consists in pushing abstraction to the limit. In the rest of this book, you’ll learn how to abstract many things so you never have to define them again. You will, for example, learn how to abstract loops so you won’t have to write loops ever again. And you’ll learn how to abstract parallelization in a way that will allow you to switch from serial to parallel processing just by selecting a method in the List class.
Summary
* Functional programming is programming with functions, returning values, and having no side effects.
* Functional programs are easy to reason about and easy to test.
* Functional programming offers a high level of abstraction and reusability.
* Functional programs are more robust than their imperative counterparts.
* Functional programs are safer in multithreading environments because they avoid shared mutable state.
Supplement
* Functional Programming with Java 8
* Functional Programming for Beginners
* An Introduction to Functional Programming in Java 8: Part 3 - Streams
沒有留言:
張貼留言