What is functional programming

A method can be functional as long as it satisfies the requirements of a pure function

  1. It can’t modify anything outside of the function. From the outside, no change can be observed from the inside.
  2. It cannot modify its own parameters.
  3. It cannot throw errors or exceptions.
  4. It must return a value.
  5. As long as it is called with the same parameters, the result must be the same.

Look at an example:

public class FunctionMethods {

    public int percent1 = 5;

    private int percent2 = 5;

    private final int percent3 = 5;

    public void setPercent2(int percent2) {
        this.percent2 = percent2;
    }

    public int add(int a, int b) {
        return a + b;
    }

    public int mult(int a, Integer b) {
        a = 5;
        b = 2;
        return a * b;
    }

    public int div(int a, int b) {
        return a / b;
    }


    public int tax1(int a) {
        return a / 100 * (100 + percent1);
    }

    public int tax2(int a) {
        return a / 100 * (100 + percent2);
    }

    public int tax3(int a) {
        return a / 100 * (100 + percent3);
    }

    public List<Integer> append(int i, List<Integer> list) {
        list.add(i);
        returnlist; }}Copy the code

Is the add method a function? Add is a function because the value it returns always depends on its arguments. It does not modify its parameters, nor does it interact with the outside world in any way. This method may fail when a+b overflows the maximum int value, but it does not throw an exception. The result is wrong (a negative number), but that’s another problem. Whenever a function is called with the same argument, the result must always be the same.

Is the mult method a function? The mult method is also a pure function, but unlike the add method, it changes the values of parameters A and b internally. Java’s basic types are passed by value, meaning that changes to parameters inside a function cannot be detected externally. This method is equivalent to a method with no parameters.

// Java's primitive types are value passed, meaning that changes to parameters inside a function cannot be detected externally
public static void main(String[] args) {
    FunctionMethods functionMethods = new FunctionMethods();
    int a = 5;
    int b = 7;
    System.out.println(functionMethods.mult(5.7));
    // A is still 5, b is still 7
    System.out.println("a -> " + a);
    System.out.println("b -> " + b);
}
Copy the code

Is the div method a function? Not a function. The div method is not a pure function because it throws an exception when a divisor is zero. To make it a function, you can check the second argument and return a value when it is 0.

Is the tax1 method a function? Not a function, because it depends on an external public variable, and the value may be inconsistent between calls. Therefore, two calls of the same parameter may return different values.

public static void main(String[] args) {
    FunctionMethods functionMethods = new FunctionMethods();
    System.out.println(functionMethods.tax1(100));
    // Modify the value of percent1
    functionMethods.percent1 = 10;
    // The same parameter returns different values
    System.out.println(functionMethods.tax1(100));
}
Copy the code

Is the tax2 method a function? Percent2 is not a function. Although percent2 is private, percent2 provides a public set method that can modify its value, so it’s not a function like Tax1

Is the Tax3 method a function? Percent3 is a function that always returns the same value in the case of the same parameters, because it only depends on its own parameters and the immutable final property percent3. You might think tax3 isn’t a pure function, because the result doesn’t just depend on the method’s arguments (the result of a pure function must depend only on its own arguments). Actually percent3 is an extra parameter. The class itself can also be considered an implicit extra parameter, because all of its attributes are accessible inside the method itself.

This is an important concept. Any instance method can become a static method by enclosing the types of an enclosing class in its parameters. For example, the tax function can be rewritten as:


public int tax3(int a) {
    return tax3(this,a);
}
// This method can be called from within the class, passing a reference to this
public static int tax3(FunctionMethods functionMethods, int a) {
    return a / 100 * (100 + functionMethods.percent3);
}
Copy the code

The append method changes the parameters before returning the result, and this change is observable outside the method, so it is not a pure function.

Advantages of functional programming

  1. Functional programs are easier to infer because they are deterministic. Always give the same output for a particular input. In many cases, you can prove that the program is correct, rather than being unsure after extensive testing whether the program will fail in unexpected circumstances.
  2. Functional programs are easier to test. Because there are no side effects, you don’t need mocks that are often used to separate applications from the outside world in tests.
  3. Functional programs are more modular because they are built from functions that have only inputs and outputs. We don’t have to deal with side effects, we don’t have to catch exceptions, we don’t have to deal with context changes, we don’t have to share changing state, and we don’t have to make concurrent changes.
  4. Functional programming makes composition and recomposition easier. To write a functional program, you need to start writing the various basic functions necessary and compound them into higher-level functions, repeating the process until you have a function that is consistent with the program you want to build. Because all functions are reference-transparent, they can be reused by other programs without modification.

Use substitution models to infer programs

A function does nothing, it just has a value that depends on arguments. Therefore, you can always replace a function call or any expression that references transparency with its value

Take a look at this code

public static int add(int a, int b) {
    System.out.println(String.format("Returning %s as 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 main(String[] args) {
    int result = add(mult(2.3),mult(4.5));
}
Copy the code

When you call add, you can completely replace add(mult(2,3),mult(4,5)) with add(6,20). Replacing mult(2,3) and mult(4,5) with their respective return values does not change the meaning of the program

But replacing the add function directly with the return value changes the meaning of the program, because the System.out.println method is no longer called and no logging is done. It may or may not be important. In short, it alters the results of the program.

Apply the functional principle to a simple example

A small example of buying bread with a credit card

public class BreadShop {

    public Bread buyBread(CreditCard creditCard){
        Bread bread = new Bread();
        // Credit card payment is a side effect in this case
        creditCard.charge(bread.getPrice());
        return bread;
    }
    public static class Bread {
        public Double getPrice(a){
            return 100D; }}}Copy the code

Credit card payments are a side effect. Credit card payment mostly consists of calling the bank, checking that the credit card is available and authorized, registering transactions, etc. Function returns bread

The problem with such code is that it is difficult to test. The test program may need to contact the bank and register the transaction with a mock account. Or you create a mock credit card to replace the real charge method and verify the mock state after testing.

If you want to test your code without touching banks or using mocks, you should remove side effects. Since you still want to pay by credit card, the only solution is to add something to the return value to indicate the operation. Your buyBread method will return the Payment and the thing that represents the Payment.

public class Payment {

    private final CreditCard creditCard;

    private final Double amount;

    public Payment(CreditCard creditCard, Double amount) {
        this.creditCard = creditCard;
        this.amount = amount;
    }
    
    public void pay(a) {
       this.creditCard.charge(amount); }}Copy the code

This class contains the necessary data to represent a payment, consisting of a credit card and the amount paid. Since the buyBread method needs to return both Donut and Payment, you need to create a special class for this

public class Purchase {

    public BreadShop.Bread bread;

    public Payment payment;

    public Purchase(BreadShop.Bread bread, Payment payment) {
        this.bread = bread;
        this.payment = payment; }}Copy the code

Often a class is needed to hold two (or more) values, because the way functional programming replaces side effects is by returning them.

Test and modify the way you buy bread

public Purchase buyBread2(CreditCard creditCard){
    Bread bread = new Bread();
    return new Purchase(bread,new Payment(creditCard,bread.getPrice()));
}
Copy the code
public void testBuyBread2(a){
    // The test calls the buyBread2 method without having to worry about credit card payments
    // You can call the credit card function later, or even not, without affecting the buyBread2 test
    Purchase purchase = new BreadShop()
            .buyBread2(new CreditCard());
    // Make the immediate payment here, or save the purchase and pay later
    purchase.payment.pay();
}
Copy the code

You don’t have to worry about how to actually pay with the credit card at this point, which gives you some freedom to build your application. You can still pay immediately, or you can save it for later payments.

It is even possible to combine multiple pending payment records held on a credit card and process them in one operation. This saves you money by reducing the number of calls to credit card services, such as combining a Payment using the following Combine method

public class Payment {

    public Payment combine(Payment payment) {
        if(payment.creditCard.equals(this.payment)){
            // If it is a credit card account, you can combine the two payments
            // Returns a new Payment
            return new Payment(this.creditCard,payment.amount + this.amount);
        }
        // If the payment account is not the same, then the exception cannot be merged
        throw new IllegalStateException("current creditCard not match"); }}Copy the code

Note that an exception will be thrown if the credit card is inconsistent. This is not inconsistent with functional programming not throwing exceptions. Here, trying to merge two pending records from two different credit cards is considered a bug, so the application must crash. Of course, this is not a very smart way to do it, but there are better ways to do it later

@Test
public void testBuyBread3(a){
    BreadShop breadShop = new BreadShop();
    CreditCard creditCard = new CreditCard();
    // First purchase
    Purchase purchase1 = breadShop.buyBread2(creditCard);
    // Second purchase
    Purchase purchase2 = breadShop.buyBread2(creditCard);

    // Combine payments
    purchase1.payment.combine(purchase2.payment).pay();
}
Copy the code