1. Overview

In this tutorial, we’ll understand the functional programming paradigm’s core principles and how to practice them in the Java programming language.

また、高度な関数型プログラミング手法のいくつかについても説明します。

This will help us evaluate the benefits we get from functional programming, especially in Java.

2. What Is Functional Programming?

Basically, functional programming is a style of writing computer programs that treat computations as evaluating mathematical functions.

In mathematics, a function is an expression that relates an input set to an output set.

重要なのは、関数の出力はその入力のみに依存するということです。 さらに興味深いことに、2つ以上の関数を一緒に作成して、新しい関数を取得できます。

2.1. ラムダ計算

To understand why these definitions and properties of mathematical functions are important in programming, we’ll have to go back in time a bit.

In the 1930s, mathematician Alonzo Church developed a formal system to express computations based on function abstraction. This universal model of computation came to be known as lambda calculus.

ラムダ計算は、プログラミング言語、特に関数型プログラミング言語の理論の開発に多大な影響を及ぼしました。 通常、関数型プログラミング言語はラムダ計算を実装します。

Since lambda calculus focuses on function composition, functional programming languages provide expressive ways to compose software in function composition.

2.2. プログラミングパラダイムの分類

もちろん、実際のプログラミングスタイルは関数型プログラミングだけではありません。 Broadly speaking, programming styles can be categorized into imperative and declarative programming paradigms.

The imperative approach defines a program as a sequence of statements that change the program’s state until it reaches the final state.

手続き型プログラミングは、手続き型またはサブルーチンを使用してプログラムを構築する命令型プログラミングの一種です。 One of the popular programming paradigms known as Object-Oriented Programming (OOP) extends procedural programming concepts.

In contrast, the declarative approach expresses the logic of a computation without describing its control flow in terms of a sequence of statements.

簡単に言えば、宣言型アプローチの焦点は、プログラムがどのように達成すべきかではなく、プログラムが何を達成しなければならないかを定義することです。 Functional programming is a subset of the declarative programming languages.

These categories have further subcategories, and the taxonomy gets quite complex, but we won’t get into that for this tutorial.

2.3. プログラミング言語の分類

Now we’ll try to understand how programming languages are divided based on their support for functional programming for our purposes.

Pure functional languages, such as Haskell, only allow pure functional programs.

Other languages allow both functional and procedural programs and are considered impure functional languages. Many languages fall into this category, including Scala, Kotlin and Java.

It’s important to understand that most of the popular programming languages today are general-purpose languages, so they tend to support multiple programming paradigms.

3. 基本的な原則と概念

This section will cover some of the basic principles of functional programming and how to adopt them in Java.

Please note that many features we’ll be using haven’t always been part of Java, and it’s advisable to be on Java 8 or later to exercise functional programming effectively.

3.1. ファーストクラスおよび高階関数

A programming language is said to have first-class functions if it treats functions as first-class citizens.

This means that functions are allowed to support all operations typically available to other entities. These include assigning functions to variables, passing them as arguments to other functions and returning them as values from other functions.

このプロパティにより、関数型プログラミングで高階関数を定義できます。 Higher-order functions are capable of receiving functions as arguments and returning a function as a result. This further enables several techniques in functional programming such as function composition and currying.

Traditionally, it was only possible to pass functions in Java using constructs such as functional interfaces or anonymous inner classes. 機能インターフェースには、抽象メソッドが1つだけあり、単一抽象メソッド(SAM)インターフェースとも呼ばれます。

Collections.sortメソッドにカスタムコンパレータを提供する必要があるとしましょう。

Collections.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer n1, Integer n2) {
        return n1.compareTo(n2);
    }
});

As we can see, this is a tedious and verbose technique — certainly not something that encourages developers to adopt functional programming.

Fortunately, Java 8 brought many new features to ease the process, such as lambda expressions, method references and predefined functional interfaces.

ラムダ式が同じタスクでどのように役立つかを見てみましょう:

Collections.sort(numbers, (n1, n2) -> n1.compareTo(n2));

This is definitely more concise and understandable.

ただし、これはJavaで一級市民として関数を使用しているような印象を与える可能性がありますが、そうではないことに注意してください。

ラムダ式のシンタックスシュガーの背後にあるJavaは、これらを機能インターフェイスにラップします。 So, Java treats a lambda expression as an Object, which is the true first-class citizen in Java.

3.2. 純粋関数

The definition of pure function emphasizes that a pure function should return a value based only on the arguments and should have no side effects.

This can sound quite contrary to all the best practices in Java.

As an object-oriented language, Java recommends encapsulation as a core programming practice. オブジェクトの内部状態を非表示にし、オブジェクトにアクセスして変更するために必要なメソッドのみを公開することをお勧めします。 So, these methods aren’t strictly pure functions.

Of course, encapsulation and other object-oriented principles are only recommendations and not binding in Java.

In fact, developers have recently started to realize the value of defining immutable states and methods without side effects.

ソートしたばかりのすべての数値の合計を求めたいとしましょう。

Integer sum(List<Integer> numbers) {
    return numbers.stream().collect(Collectors.summingInt(Integer::intValue));
}

This method depends only on the arguments it receives, so it’s deterministic. また、副作用もありません。

副作用は、メソッドの意図された動作とは別のものである可能性があります。 For instance, side effects can be as simple as updating a local or global state or saving to a database before returning a value. (Purists also treat logging as a side effect.)

So, let’s look at how we deal with legitimate side effects. たとえば、真の理由で結果をデータベースに保存する必要がある場合があります。 There are techniques in functional programming to handle side effects while retaining pure functions.

それらのいくつかについては、後のセクションで説明します。

3.3. 不変性

Immutability is one of the core principles of functional programming, and it refers to the property that an entity can’t be modified after being instantiated.

In a functional programming language, this is supported by design at the language level. But in Java we have to make our own decision to create immutable data structures.

Java自体には、 String など、いくつかの組み込みの不変型が用意されていることに注意してください。 This is primarily for security reasons because we heavily use String in class loading and as keys in hash-based data structures. There are also several other built-in immutable types such as primitive wrappers and math types.

しかし、Javaで作成するデータ構造についてはどうでしょうか。 Of course, they are not immutable by default, and we have to make a few changes to achieve immutability.

最後のキーワード使用はその1つですが、それだけではありません。

public class ImmutableData {
    private final String someData;
    private final AnotherImmutableData anotherImmutableData;
    public ImmutableData(final String someData, final AnotherImmutableData anotherImmutableData) {
        this.someData = someData;
        this.anotherImmutableData = anotherImmutableData;
    }
    public String getSomeData() {
        return someData;
    }
    public AnotherImmutableData getAnotherImmutableData() {
        return anotherImmutableData;
    }
}

public class AnotherImmutableData {
    private final Integer someOtherData;
    public AnotherImmutableData(final Integer someData) {
        this.someOtherData = someData;
    }
    public Integer getSomeOtherData() {
        return someOtherData;
    }
}

Note that we have to diligently observe a few rules:

  • All fields of an immutable data structure must be immutable.
  • This must apply to all the nested types and collections (including what they contain) as well.
  • There should be one or more constructors for initialization as needed.
  • There should only be accessor methods, possibly with no side effects.

It’s not easy to get it completely right every time, especially when the data structures start to get complex.

ただし、いくつかの外部ライブラリを使用すると、Javaでの不変データの操作が簡単になります。 For instance, Immutables and Project Lombok provide ready-to-use frameworks for defining immutable data structures in Java.

3.4. 参照透過性

Referential transparency is perhaps one of the more difficult principles of functional programming to understand, but the concept is pretty simple.

We call an expression referentially transparent if replacing it with its corresponding value has no impact on the program’s behavior.

This enables some powerful techniques in functional programming such as higher-order functions and lazy evaluation.

これをよりよく理解するために、例を見てみましょう。

public class SimpleData {
    private Logger logger = Logger.getGlobal();
    private String data;
    public String getData() {
        logger.log(Level.INFO, "Get data called for SimpleData");
        return data;
    }
    public SimpleData setData(String data) {
        logger.log(Level.INFO, "Set data called for SimpleData");
        this.data = data;
        return this;
    }
}

This is a typical POJO class in Java, but we’re interested in finding if this provides referential transparency.

次のステートメントを観察してみましょう。

String data = new SimpleData().setData("Baeldung").getData();
logger.log(Level.INFO, new SimpleData().setData("Baeldung").getData());
logger.log(Level.INFO, data);
logger.log(Level.INFO, "Baeldung");

The three calls to logger are semantically equivalent but not referentially transparent.

The first call is not referentially transparent since it produces a side effect. この呼び出しを3番目の呼び出しのようにその値に置き換えると、ログが失われます。

The second call is also not referentially transparent since SimpleData is mutable. プログラム内の任意の場所でdata.setDataを呼び出すと、その値に置き換えることが困難になります。

So, for referential transparency, we need our functions to be pure and immutable. These are the two preconditions we discussed earlier.

参照透過性の興味深い結果として、文脈自由コードを作成します。 In other words, we can run them in any order and context, which leads to different optimization possibilities.

4. 関数型プログラミング技術

The functional programming principles that we discussed earlier enable us to use several techniques to benefit from functional programming.

このセクションでは、これらの一般的な手法のいくつかを取り上げ、Javaでそれらを実装する方法を理解します。

4.1. 機能構成

Function composition refers to composing complex functions by combining simpler functions.

This is primarily achieved in Java using functional interfaces, which are target types for lambda expressions and method references.

Typically, any interface with a single abstract method can serve as a functional interface. So, we can define a functional interface quite easily.

However, Java 8 provides us many functional interfaces by default for different use cases under the package java.util.function.

これらの機能インターフェイスの多くは、デフォルトおよび静的メソッドの観点から機能合成をサポートします。 Let’s pick the Function interface to understand this better.

Function is a simple and generic functional interface that accepts one argument and produces a result.

また、composeandThenの2つのデフォルトのメソッドを提供します。これは、関数の合成に役立ちます。

Function<Double, Double> log = (value) -> Math.log(value);
Function<Double, Double> sqrt = (value) -> Math.sqrt(value);
Function<Double, Double> logThenSqrt = sqrt.compose(log);
logger.log(Level.INFO, String.valueOf(logThenSqrt.apply(3.14)));
// Output: 1.06
Function<Double, Double> sqrtThenLog = sqrt.andThen(log);
logger.log(Level.INFO, String.valueOf(sqrtThenLog.apply(3.14)));
// Output: 0.57

これらの方法はどちらも、複数の関数を1つの関数に構成することを可能にしますが、異なるセマンティクスを提供します。 compose は最初に引数で渡された関数を適用し、次にそれが呼び出された関数を適用しますが、andThenは逆に同じことを行います。

Several other functional interfaces have interesting methods to use in function composition, such as the default methods and, or and negate in the Predicate interface. While these functional interfaces accept a single argument, there are two-arity specializations, such as BiFunction and BiPredicate.

4.2. モナド

Many of the functional programming concepts derive from Category Theory, which is a general theory of functions in mathematics. It presents several concepts of categories such as functors and natural transformations.

私たちにとって重要なのは、これが関数型プログラミングでモナドを使用するための基礎であることを知っていることだけです。

正式には、モナドはプログラムを一般的に構造化することを可能にする抽象化です。 So, a monad allows us to wrap a value, apply a set of transformations, and get the value back with all transformations applied.

Of course, there are three laws that any monad needs to follow — left identity, right identity and associativity — but we won’t get into the details here.

In Java, there are a few monads that we use quite often, such as Optional and Stream:

Optional.of(2).flatMap(f -> Optional.of(3).flatMap(s -> Optional.of(f + s)))

Why do we call Optional a monad?

Here Optional allows us to wrap a value using the method of and apply a series of transformations. We’re applying the transformation of adding another wrapped value using the method flatMap.

We could show that Optional follows the three laws of monads. However, an Optional does break the monad laws under some circumstances. But it should be good enough for us for most practical situations.

If we understand monads’ basics, we’ll soon realize that there are many other examples in Java, such as Stream and CompletableFuture. それらは私たちがさまざまな目的を達成するのに役立ちますが、それらはすべて、コンテキスト操作または変換が処理される標準的な構成を持っています。

Of course, we can define our own monad types in Java to achieve different objectives such as log monad, report monad or audit monad. For example, the monad is one of the functional programming techniques to handle side effects in functional programming.

4.3. カリー化

Currying is a mathematical technique of converting a function that takes multiple arguments into a sequence of functions that take a single argument.

In functional programming, it gives us a powerful composition technique where we don’t need to call a function with all its arguments.

さらに、カレー関数は、すべての引数を受け取るまでその効果を実現しません。

In pure functional programming languages such as Haskell, currying is well supported. In fact, all functions are curried by default.

However, in Java it’s not that straightforward:

Function<Double, Function<Double, Double>> weight = mass -> gravity -> mass * gravity;

Function<Double, Double> weightOnEarth = weight.apply(9.81);
logger.log(Level.INFO, "My weight on Earth: " + weightOnEarth.apply(60.0));

Function<Double, Double> weightOnMars = weight.apply(3.75);
logger.log(Level.INFO, "My weight on Mars: " + weightOnMars.apply(60.0));

Here we’ve defined a function to calculate our weight on a planet. While our mass remains the same, gravity varies by the planet we’re on.

We は、重力だけを渡して特定の惑星の関数を定義することにより、関数を部分的に適用できます。 さらに、この部分的に適用された関数を、任意の構成の引数または戻り値として渡すことができます。

Currying depends on the language to provide two fundamental features: lambda expressions and closures. Lambda expressions are anonymous functions that help us to treat code as data. 機能インターフェイスを使用してそれらを実装する方法については、前に説明しました。

A lambda expression may close upon its lexical scope, which we define as its closure.

例を見てみましょう:

private static Function<Double, Double> weightOnEarth() {	
    final double gravity = 9.81;	
    return mass -> mass * gravity;
}

上記のメソッドで返すラムダ式が、クロージャと呼ばれる囲んでいる変数にどのように依存するかに注意してください。 Unlike other functional programming languages, Java has a limitation that the enclosing scope has to be final or effectively final.

興味深い結果として、カリー化により、Javaで任意のアリティの機能インターフェイスを作成することもできます。

4.4. 再帰

Recursion is another powerful technique in functional programming that allows us to break down a problem into smaller pieces. The main benefit of recursion is that it helps us eliminate the side effects, which is typical of any imperative style looping.

再帰を使用して数値の階乗を計算する方法を見てみましょう。

Integer factorial(Integer number) {
    return (number == 1) ? 1 : number * factorial(number - 1);
}

Here we call the same function recursively until we reach the base case and then start to calculate our result.

各ステップで結果を計算する前に、または計算の先頭にある単語で、再帰呼び出しを行っていることに注意してください。 So, this style of recursion is also known as head recursion.

このタイプの再帰の欠点は、基本ケースに到達するまで、すべてのステップが前のすべてのステップの状態を保持する必要があることです。 これは実際には少数の問題ではありませんが、多数の状態を保持することは非効率的です。

A solution is a slightly different implementation of the recursion known as tail recursion. Here we ensure that the recursive call is the last call a function makes.

末尾再帰を使用するように上記の関数を書き直す方法を見てみましょう。

Integer factorial(Integer number, Integer result) {
    return (number == 1) ? result : factorial(number - 1, result * number);
}

関数でアキュムレータが使用されていることに注意してください。これにより、再帰のすべてのステップで状態を保持する必要がなくなります。 このスタイルの本当の利点は、コンパイラーが現在の関数のスタックフレームを解放することを決定できるコンパイラーの最適化を活用することです。これは、末尾呼び出しの除去として知られる手法です。

While many languages such as Scala support tail-call elimination, Java still does not have support for this. これはJavaのバックログの一部であり、 ProjectLoomで提案されたより大きな変更の一部として何らかの形で提供される可能性があります。

5. Why Functional Programming Matters

By now, we might wonder why we even want to make this much effort. For someone coming from a Java background, the shift that functional programming demands is not trivial. So, there should be some really promising advantages for adopting functional programming in Java.

The biggest advantage of adopting functional programming in any language, including Java, is pure functions and immutable states. If we think back, most of the programming challenges are rooted in the side effects and mutable state one way or the other. Simply getting rid of them makes our program easier to read, reason about, test and maintain.

Declarative programming leads to very concise and readable programs. As a subset of declarative programming, functional programming offers several constructs such as higher-order functions, function composition and function chaining. StreamAPIがデータ操作を処理するためにJava8にもたらした利点について考えてみてください。

ただし、完全に準備ができていない限り、切り替えたくはありません。 Please note that functional programming is not a simple design pattern that we can immediately use and benefit from.

関数型プログラミングは、問題とその解決策についての推論方法と、アルゴリズムの構造化方法をさらに変更したものです。

したがって、関数型プログラミングを使い始める前に、関数の観点からプログラムについて考えるように訓練する必要があります。

6. Javaは適切ですか?

It’s hard to deny functional programming benefits, but is Java a suitable choice for it?

Historically, Java evolved as a general-purpose programming language more suitable for object-oriented programming. Even thinking about using functional programming before Java 8 was tedious! しかし、Java 8以降、状況は確実に変化しました。

The fact that there are no true function types in Java goes against functional programming’s basic principles. The functional interfaces disguised as lambda expressions largely make up for it, at least syntactically.

Then it doesn’t help that types in Java are inherently mutable and we have to write so much boilerplate to create immutable types.

Javaに欠けている、または難しい関数型プログラミング言語には、他のことが期待されます。 For instance, the default evaluation strategy for arguments in Java is eager. But lazy evaluation is a more efficient and recommended way in functional programming.

Javaでは、オペレーターの短絡と機能インターフェイスを使用して遅延評価を実行できますが、より複雑になります。

The list is certainly not complete and can include generics support with type-erasure, missing support for tail-call optimization and other things. However, we get a broad idea.

Java is definitely not suitable for starting a program from scratch in functional programming.

しかし、Javaで、おそらくオブジェクト指向プログラミングで書かれた既存のプログラムがすでにある場合はどうでしょうか。 特にJava8では、関数型プログラミングの利点のいくつかを得るのを妨げるものは何もありません。

これは、関数型プログラミングの利点のほとんどがJava開発者にとってのメリットです。 A combination of object-oriented programming with the benefits of functional programming can go a long way.

7. 結論

In this article, we went through the basics of functional programming. We covered the fundamental principles and how we can adopt them in Java.

さらに、Javaの例を使用して、関数型プログラミングで一般的な手法について説明しました。

最後に、関数型プログラミングを採用することの利点のいくつかを取り上げ、Javaがそれに適しているかどうかについて回答しました。

この記事のソースコードは、GitHubから入手できます。