Java coding practices for functional programming: writing high-performance, abstract code using laziness

Posted by EriRyoutan on Mon, 01 Nov 2021 18:49:16 +0100

Author|Hanging
Source|Ali Technical Public Number

This article takes lazy loading as an example to step through the concepts of functional programming, so the reader does not need any basis for functional programming, just a little knowledge of Java 8.

Is an abstraction bound to degrade code performance?

The programmer's dream is to write "high cohesion, low coupling" code, but empirically, more abstract code often means less performance. Machines can directly execute the most powerful assemblies, followed by C, and Java has a lower performance due to a higher level of abstraction. Business systems are also constrained by the same rules, with the top-level business interfaces having the highest performance because of the addition of various business checks and message sending.

Performance concerns also limit programmers'more reasonable abstraction of modules.

Consider a common system abstraction, user is a common entity in the system. In order to unify the user abstraction in the system, we define a common domain model User, which contains not only the user's id, but also Department information, the user's supervisor and so on. These are attributes that are often used together in the system:

public class User {
    // User id
    private Long uid;
    // User's department, to keep the example simple, use the normal string here
    // Require remote call to address book system to get
    private String department;
    // The user's supervisor, to keep the example simple, is represented by an id
    // Require remote call to address book system to get
    private Long supervisor;
    // Permissions held by the user
    // Require remote call privilege system to get
    private Set< String> permission;
}

This looks great, "Users "Common attributes are all concentrated in one entity. As long as this User is used as a parameter of the method, the method will no longer need to query other user information. However, once implemented, problems will be found. Departments and directors need to call the Address Book system remotely to get access, and privileges need to be obtained by calling the privilege system remotely. Every time a User is constructed, it has to pay for it The cost of these two remote calls, even if some information is not used. This is illustrated by the following method (to determine whether a user is the head of another user):

public boolean isSupervisor(User u1, User u2) {
    return Objects.equals(u1.getSupervisor(), u2.getUid());
}

In order to be able to use the generic User entity in this method parameter above, an additional cost must be paid: the remote call obtains entirely unused privilege information, and if there is a problem with the privilege system, it also affects the stability of the unrelated interface.

Thinking about this, we might want to abandon the common entity scheme, let the bare uid pervade the system, and scatter user information query codes around the system.

In fact, with a little improvement, you can continue to use the abstraction above. You only need to turn department, supervisor, and permission all into lazily loaded fields and make external calls when needed, which has a lot of benefits:

  • Business modeling really decouples the business layer from the physical layer by considering business fit rather than underlying performance issues
  • Business logic is separated from external calls, and no matter how the external interface changes, we always have an adapter layer to keep the core logic stable
  • Business logic appears to be purely physical, easy to write unit tests, and guarantees the correctness of core logic

However, there are often some problems in the process of practice. This paper combines some techniques of Java and functional programming to implement a lazy loading tool class.

Two strictness and inertia: the nature of Java 8 Supplier

Java 8 introduces a new functional interface, Supplier, which, from the perspective of older Java programmers, is simply an interface that can get any value. Lambda is just a syntax sugar for its implementation class. This is an understanding standing in a linguistic rather than a computational angle. When you understand the difference between strict and lazy, you may have a closer view of the nature of computing.

Because Java and C are strict programming languages, we're used to having variables calculate where they are defined. In fact, there is another genre of programming languages that compute when variables are used, such as Haskell, a functional programming language.

So the essence of Supplier is to introduce the mechanism of lazy computing in the Java language. In order to achieve the equivalent lazy computing in Java, you can write as follows:

Supplier< Integer> a = () -> 10 + 1;
int b = a.get() + 1;

Further optimization of the three Supplier s: Lazy

Supplier also has the problem of recalculating every time it gets a value from a get, and true lazy calculations should cache the value after the first get. As long as the Supplier is slightly packaged:

/**
* Lazy also implements Supplier for easy interaction with standard Java functional interfaces
*/
public class Lazy< T> implements Supplier< T> {

    private final Supplier< ? extends T> supplier;
    
    // Cache the calculated value of supplier using the value property
    private T value;

    private Lazy(Supplier< ? extends T> supplier) {
        this.supplier = supplier;
    }

    public static < T> Lazy< T> of(Supplier< ? extends T> supplier) {
        return new Lazy< >(supplier);
    }

    public T get() {
        if (value == null) {
            T newValue = supplier.get();

            if (newValue == null) {
                throw new IllegalStateException("Lazy value can not be null!");
            }

            value = newValue;
        }

        return value;
    }
}

Write previous lazy calculation code with Lazy:

Lazy< Integer> a = Lazy.of(() -> 10 + 1);
int b = a.get() + 1;
// get does not recalculate, it uses cached values directly
int c = a.get();

Optimize our previous generic user entities with this lazy loading tool class:

public class User {
    // User id
    private Long uid;
    // User's department, to keep the example simple, use the normal string here
    // Require remote call to address book system to get
    private Lazy< String> department;
    // The user's supervisor, to keep the example simple, is represented by an id
    // Require remote call to address book system to get
    private Lazy< Long> supervisor;
    // User's Permissions
    // Require remote call privilege system to get
    private Lazy< Set< String>> permission;
    
    public Long getUid() {
        return uid;
    }
    
    public void setUid(Long uid) {
        this.uid = uid;
    }
    
    public String getDepartment() {
        return department.get();
    }
    
    /**
    * Because department is a lazily loaded property, the set method must pass in a calculation function, not a specific value
    */
    public void setDepartment(Lazy< String> department) {
        this.department = department;
    }
    // ...similar omissions after
}

A simple example of constructing a User entity is as follows:

Long uid = 1L;
User user = new User();
user.setUid(uid);
// departmentService is an rpc call
user.setDepartment(Lazy.of(() -> departmentService.getDepartment(uid)));
// ....

This looks good, but as you continue to work in depth, you will find some issues: the user's two attributes departments and directors are related, and you need to get the user departments through the rpc interface, and then the directors from the Department through another rpc interface. The code is as follows:

String department = departmentService.getDepartment(uid);
Long supervisor = SupervisorService.getSupervisor(department);

But now department is no longer a computed value, but a lazy Lazy object. How should the code above be written? Letters are designed to solve this problem

Four Lazy Implementation Letters

Quick understanding: Similar to the stream api in Java or the map method in Optional. Letters can be understood as an interface, and maps can be understood as methods in an interface.

1 Letter Computed Object

Collection < T>, Optional < T> in Java, and the Lazy < T> we just implemented all share the same feature: they all have one and only one generic parameter, which we will temporarily call a box in this article and write it down as a box < T> because they are all like containers that can be packaged in any type.

Definition of 2 Letters

Letter operations can apply a function that maps T to S to Box < T> so that it becomes Box < S>. An example of converting a number in Box to a string is as follows:

The reason boxes contain types instead of 1 and 1 is that boxes don't necessarily have individual values, such as collections, or even more complex multivalue mappings.

It is important to note that not simply defining a signature that satisfies Box < S> map (Function < T, S > function) makes Box < T> a letter, here is a counterexample:

// The inverse cannot be a letter because this method does not faithfully reflect the function al mapping in the box
public Box< S> map(Function< T,S> function) {
    return new Box< >(null);
}

So a letter is a more stringent definition than the map method, and he also requires that a map satisfy the following laws, called the letter law (the essence of the law is to ensure that the map method faithfully reflects the mapping relationship defined by the parameter function):

  • Unit Law: Box < T > When an identity function is applied, the value does not change, that is, box.equals (box.map (Function.identity()) is always true (here equals is just a mathematical equivalent to what you want to express)
  • Law of Composition: Assume that there are two functions F1 and f2, map (x -> F2 (f1 (x)) and map(f1).map(f2) are always equivalent

It is clear that Lazy satisfies both of these laws.

3 Lazy Letter

Although so many theories have been introduced, the implementation is very simple:

    public < S> Lazy< S> map(Function< ? super T, ? extends S> function) {
        return Lazy.of(() -> function.apply(get()));
    }

It is easy to prove that it satisfies the law of letters.

With map, we can easily solve the problems we encountered before. The functions passed in from map can operate on the assumption that department information has been obtained:

Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
Lazy< Long> supervisorLazy = departmentLazy.map(
    department -> SupervisorService.getSupervisor(department)
);

4 More difficult situation encountered

Now we can not only construct the value of inertia, but also calculate another value of inertia with one value, which looks perfect. But when you go deeper, you find more difficult problems.

I now need two parameters, department and supervisor, to invoke the permission system to gain permissions, and both values are lazy. First try using nested map s:

Lazy< Lazy< Set< String>>> permissions = departmentLazy.map(department ->
         supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);

The type of return value seems a bit odd. We expect Lazy < Set < String>, but one more layer here becomes Lazy < Lazy < Set < String >>. As you increase the number of layers of nested map s, Lazy's generic level also increases, as shown in the following three parameters:

Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Lazy< Lazy< Long>>> result = param1Lazy.map(param1 ->
        param2Lazy.map(param2 ->
                param3Lazy.map(param3 -> param1 + param2 + param3)
        )
);

This is solved by the following mono-operation.

Five Lazy Monad

Quick Understanding: Similar to flatmap functionality in Java stream api and Optional

Definition of 1 Monon

A major difference between a singleton and a letter is the function that is received. Functions of a letter generally return native values, while functions of a singleton return boxed values. The function in the figure below, if map is used instead of flatmap, results in a Russian doll, a two-layer box.

Mono certainly has the law of Mono, but it is more complicated than the law of letters, which is not explained here. Its role is similar to that of letters, ensuring that flatmap can truthfully reflect the mapping relationship of function s.

2 Lazy Monons

It's also easy to do:

    public < S> Lazy< S> flatMap(Function< ? super T, Lazy< ? extends S>> function) {
        return Lazy.of(() -> function.apply(get()).get());
    }

Use flatmap to solve previous problems:

Lazy< Set< String>> permissions = departmentLazy.flatMap(department ->
         supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);

The case of three parameters:

Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Long> result = param1Lazy.flatMap(param1 ->
        param2Lazy.flatMap(param2 ->
                param3Lazy.map(param3 -> param1 + param2 + param3)
        )
);

The rule is that map is used for the last value and flatmap for the rest.

3 off-topic: monogrammatical sugar in functional languages

Looking at the example above, you will surely find it difficult to calculate inertia, which requires flatmap and map to go through several times each time in order to get the inertia value inside. This is a compromise that Java does not natively support functional programming. Haskell supports the use of do notation to simplify the operation of Monad. Examples of the three parameters above are written in Haskell:

do
    param1 < - param1Lazy
    param2 < - param2Lazy
    param3 < - param3Lazy
    -- Notes: do Notation return Meaning and Java Quite different
    -- It means packaging values into boxes,
    -- Equivalent Java She'll be apples Lazy.of(() -> param1 + param2 + param3)
    return param1 + param2 + param3

There is no grammatical sugar in Java, but God closes a door and opens a window. You can clearly see what each step is doing in Java and understand how it works. If you've read earlier in this article, you'll know that the do notation is flatmap.

Final Code for Six Lazy

So far, we've written the following Lazy code:

public class Lazy< T> implements Supplier< T> {

    private final Supplier< ? extends T> supplier;

    private T value;

    private Lazy(Supplier< ? extends T> supplier) {
        this.supplier = supplier;
    }

    public static < T> Lazy< T> of(Supplier< ? extends T> supplier) {
        return new Lazy< >(supplier);
    }

    public T get() {
        if (value == null) {
            T newValue = supplier.get();

            if (newValue == null) {
                throw new IllegalStateException("Lazy value can not be null!");
            }

            value = newValue;
        }

        return value;
    }

    public < S> Lazy< S> map(Function< ? super T, ? extends S> function) {
        return Lazy.of(() -> function.apply(get()));
    }

    public < S> Lazy< S> flatMap(Function< ? super T, Lazy< ? extends S>> function) {
        return Lazy.of(() -> function.apply(get()).get());
    }
}

Seven Construct an entity that automatically optimizes performance

Using Lazy, we write a factory that constructs a generic User entity:

@Component
public class UserFactory {
    
    // Departmental Services, rpc Interface
    @Resource
    private DepartmentService departmentService;
    
    // Supervisor Service, rpc Interface
    @Resource
    private SupervisorService supervisorService;
    
    // Permission Service, rpc Interface
    @Resource
    private PermissionService permissionService;
    
    public User buildUser(long uid) {
        Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
        // Get supervisors through Department
        // department -> supervisor
        Lazy< Long> supervisorLazy = departmentLazy.map(
            department -> SupervisorService.getSupervisor(department)
        );
        // Obtain authority through departments and directors
        // department, supervisor -> permission
        Lazy< Set< String>> permissionsLazy = departmentLazy.flatMap(department ->
            supervisorLazy.map(
                supervisor -> permissionService.getPermissions(department, supervisor)
            )
        );
        
        User user = new User();
        user.setUid(uid);
        user.setDepartment(departmentLazy);
        user.setSupervisor(supervisorLazy);
        user.setPermissions(permissionsLazy);
    }
}

A factory class is the construction of an evaluation tree that clearly shows the evaluation dependencies among User attributes, and the User object automatically optimizes performance at run time. Once a node is evaluated, the values of all attributes on the path are cached.

Eight exception handling

Although we use laziness to make user.getDepartment() seem like a pure memory operation, it is actually a remote call, so unexpected exceptions such as timeouts can occur.

Exception handling must not be delegated to business logic, which will affect the purity of business logic and leave us behind. An ideal way to do this is to give the lazy value the load logic Supplier. Take full account of exceptions in Spriler's computing logic and retry or throw exceptions. Although throwing exceptions may not be that "functional", they are close to Java programming habits and should interrupt business logic if key values are not available.

Nine Summary

Entities constructed with this method can place all the attributes required in business modeling. Business modeling only needs to consider business fit, not the underlying performance issues, to truly decouple the business layer from the physical layer.

At the same time, UserFactory is essentially an adapter layer for an external interface. Once the external interface changes, you only need to modify the adapter layer to protect the stability of the core business code.

Business core code is easier to write unit tests because of the reduced number of external calls and the proximity of code to pure operation. Unit tests ensure that the core code is stable and error-free.

Ten extra topics: missing Curitization and Application Letters in Java

Think carefully that you've just done so much to make a function signed as C f(A,B) work unnecessarily on box types Box < A > and Box < B > and produce a Box < C>, a more convenient way to use functional languages is to apply letters.

It is conceptually very simple to apply a boxed function to a boxed value and get a boxed value, which can be done in Lazy:

    // Notice that the function here is installed inside lazy
    public < S> Lazy< S> apply(Lazy< Function< ? super T, ? extends S>> function) {
        return Lazy.of(() -> function.get().apply(get()));
    }

However, it is not helpful to implement this in Java because Java does not support Curitization.

Curitization allows us to pin down several parameters of a function to become a new function. If the function is signed as f(a,b), the language that supports it allows direct f(a) calls, then the return value is a function that only receives B.

In support of Curitization, ordinary functions can be applied to boxed types with only a few consecutive applications of letters. An example of Haskell is as follows (< *> is the grammatical sugar used in Haskell to apply letters, f is a function signed as c f(a, b). The grammar is not entirely correct, just to express one meaning):

-- Notes: The result is box c
box f < *> box a < *> box b

Reference material

  • Similar Lazy implementations are provided in the Java Functional Class Library VAVR, but if you're just using this class, it's still a bit heavy to introduce the entire library, so you can implement it yourself using the ideas in this article
  • Advances in Functional Programming: A Functional Programming Article that applies the angle of the front end of a letter, this paper refers to the analogy method of the inner box to some extent: https://juejin.cn/post/6891820537736069134?spm=ata.21736010.0.0.595242a7a98f3U
  • Haskell Fundamentals of Functional Programming
  • Java Functional Programming

Do programmers want specialization or breadth?

click here See details!

Topics: Java C Front-end Programmer api