Java開発者のための関数型プログラミング、パート1

Java 8は、Java開発者にラムダ式を使用した関数型プログラミングを紹介しました。このJavaリリースは、Javaプログラミングについて、命令型のオブジェクト指向の観点からのみ考えるだけではもはや十分ではないことを開発者に効果的に通知しました。Java開発者は、宣言型関数型パラダイムを使用して考え、コーディングできる必要もあります。

このチュートリアルでは、関数型プログラミングの基本について説明します。用語から始めて、関数型プログラミングの概念を掘り下げます。最後に、5つの関数型プログラミング手法を紹介します。これらのセクションのコード例では、純粋関数、高階関数、遅延評価、クロージャ、およびカリー化を開始できます。

関数型プログラミングは増加しています

米国電気電子学会(IEEE)は、2018年の上位25のプログラミング言語に関数型プログラミング言語を含めました。GoogleTrendsは現在、関数型プログラミングをオブジェクト指向プログラミングよりも人気があるとランク付けしています。

明らかに、関数型プログラミングは無視できませんが、なぜそれがより一般的になっているのですか?特に、関数型プログラミングを使用すると、プログラムの正当性を簡単に検証できます。また、並行プログラムの作成を簡素化します。同時実行(または並列処理)は、アプリケーションのパフォーマンスを向上させるために不可欠です。

ダウンロードコードを取得するこのチュートリアルのサンプルアプリケーションのソースコードをダウンロードします。JavaWorld用にJeffFriesenによって作成されました。

関数型プログラミングとは何ですか?

コンピュータは通常、フォンノイマンアーキテクチャを実装します。これは、数学者および物理学者のジョンフォンノイマン(およびその他)による1945年の記述に基づいて広く使用されているコンピュータアーキテクチャです。このアーキテクチャは、命令型プログラミングに偏っています。命令型プログラミングは、ステートメントを使用してプログラムの状態を変更するプログラミングパラダイムです。C、C ++、およびJavaはすべて命令型プログラミング言語です。

1977年、著名なコンピューター科学者のジョン・バッカス(FORTRANでの彼の研究で有名)は、「プログラミングをフォンノイマン型から解放できるか」というタイトルの講演を行いました。Backusは、フォンノイマンアーキテクチャとそれに関連する命令型言語には根本的な欠陥があると主張し、ソリューションとして機能レベルのプログラミング言語(FP)を提示しました。

バッカスの明確化

Backusの講義は数十年前に発表されたため、そのアイデアのいくつかは理解しにくいかもしれません。ブロガーのTomaszJaskułaは、2018年1月のブログ投稿に明快さと脚注を追加しています。

関数型プログラミングの概念と用語

関数型プログラミングは、計算が関数型プログラミング関数として体系化されるプログラミングスタイルです。これらは、式のコンテキストで評価される数学関数のような構成(ラムダ関数など)です。

関数型プログラミング言語は宣言型です。つまり、計算のロジックは、その制御フローを記述せずに表現されます。宣言型プログラミングでは、ステートメントはありません。代わりに、プログラマーは式を使用してコンピューターに何をする必要があるかを伝えますが、タスクを実行する方法は伝えません。 SQLまたは正規表現に精通している場合は、宣言型スタイルの経験があります。どちらも、ステートメントを使用して実行方法を説明するのではなく、式を使用して実行する必要があることを記述します。

計算関数型プログラミングでは、式コンテキストで評価された関数によって記述されます。これらの関数は、値を返すJavaメソッドなど、命令型プログラミングで使用される関数と同じではありません。代わりに、関数型プログラミング関数は数学関数のようなもので、通常は引数のみに依存する出力を生成します。関数型プログラミング関数が同じ引数で呼び出されるたびに、同じ結果が得られます。関数型プログラミングの関数は、参照透過性を示すと言われています。これは、計算の意味を変更せずに、関数呼び出しをその結果の値に置き換えることができることを意味します。

関数型プログラミングは不変性を優先します。つまり、状態は変更できません。これは通常、命令型関数が状態(Javaインスタンス変数など)に関連付けられている可能性がある命令型プログラミングには当てはまりません。同じ引数を使用して異なる時間にこの関数を呼び出すと、この場合は状態が変更可能であるため、異なる戻り値が返される可能性があります。

命令型および関数型プログラミングの副作用

状態の変化は命令型プログラミングの副作用であり、参照透過性を妨げます。特にプログラムで命令型または関数型のどちらのスタイルを使用するかを評価する場合は、知っておく価値のある他の多くの副作用があります。

命令型プログラミングの一般的な副作用の1つは、代入ステートメントが格納されている値を変更して変数を変更する場合です。関数型プログラミングの関数は、変数の割り当てをサポートしていません。変数の初期値は決して変更されないため、関数型プログラミングはこの副作用を排除します。

別の一般的な副作用は、スローされた例外に基づいて命令関数の動作を変更するときに発生します。これは、呼び出し元との観察可能な相互作用です。詳細については、スタックオーバーフローの説明「例外の発生が副作用である理由」を参照してください。

3番目の一般的な副作用は、I / O操作が読み取り不可能なテキストを入力した場合、または書き込み不可能なテキストを出力した場合に発生します。StackExchangeのディスカッション「IOは関数型プログラミングでどのように副作用を引き起こすことができますか?」を参照してください。この副作用についてもっと学ぶために。

副作用を排除することで、計算動作の理解と予測がはるかに簡単になります。また、コードを並列処理により適したものにするのに役立ち、アプリケーションのパフォーマンスが向上することがよくあります。関数型プログラミングには副作用がありますが、一般的には命令型プログラミングよりも副作用が少なくなります。関数型プログラミングを使用すると、理解、保守、およびテストが容易で、再利用しやすいコードを作成するのに役立ちます。

関数型プログラミングの起源(および発信者)

関数型プログラミングは、アロンゾチャーチによって導入されたラムダ計算に端を発しています。もう1つの起源は、MosesSchönfinkelによって導入され、その後HaskellCurryによって開発されたコンビネータ論理です。

オブジェクト指向プログラミングと関数型プログラミング

私は、コードを書くための命令型、オブジェクト指向宣言型の関数型プログラミングアプローチを対比するJavaアプリケーションを作成しました。以下のコードを調べてから、2つの例の違いを指摘します。

リスト1.Employees.java

import java.util.ArrayList; import java.util.List; public class Employees { static class Employee { private String name; private int age; Employee(String name, int age) { this.name = name; this.age = age; } int getAge() { return age; } @Override public String toString() { return name + ": " + age; } } public static void main(String[] args) { List employees = new ArrayList(); employees.add(new Employee("John Doe", 63)); employees.add(new Employee("Sally Smith", 29)); employees.add(new Employee("Bob Jone", 36)); employees.add(new Employee("Margaret Foster", 53)); printEmployee1(employees, 50); System.out.println(); printEmployee2(employees, 50); } public static void printEmployee1(List employees, int age) { for (Employee emp: employees) if (emp.getAge() < age) System.out.println(emp); } public static void printEmployee2(List employees, int age) { employees.stream() .filter(emp -> emp.age  System.out.println(emp)); } }

リスト1は、EmployeesいくつかのEmployeeオブジェクトを作成し、50歳未満のすべての従業員のリストを出力するアプリケーションを示しています。このコードは、オブジェクト指向と関数型プログラミングの両方のスタイルを示しています。

このprintEmployee1()方法は、命令型のステートメント指向のアプローチを明らかにします。指定されているように、このメソッドは従業員のリストを反復処理し、各従業員の年齢を引数値と比較し、(年齢が引数よりも小さい場合)、従業員の詳細を出力します。

このprintEmployee2()メソッドは、この場合はStreams APIで実装された、宣言型の式指向アプローチを明らかにします。従業員を印刷する方法を(段階的に)命令的に指定する代わりに、式は目的の結果を指定し、それを行う方法の詳細をJavaに任せます。考えるfilter()の機能的等価物としてifの文、およびforEach()などの機能と同等forのステートメント。

リスト1は次のようにコンパイルできます。

javac Employees.java

次のコマンドを使用して、結果のアプリケーションを実行します。

java Employees

出力は次のようになります。

Sally Smith: 29 Bob Jone: 36 Sally Smith: 29 Bob Jone: 36

関数型プログラミングの例

次のセクションでは、関数型プログラミングで使用される5つのコア手法、つまり純粋関数、高階関数、遅延評価、クロージャ、およびカリー化について説明します。このセクションの例はJavaScriptでコーディングされています。これは、Javaに比べて単純であるため、テクニックに集中できるためです。パート2では、Javaコードを使用してこれらの同じ手法を再検討します。

リスト2は、RunScriptJavaのScriptingAPIを使用してJavaScriptコードの実行を容易にするJavaアプリケーションであるtoのソースコードを示しています。RunScript今後のすべての例の基本プログラムになります。

リスト2.RunScript.java

import java.io.FileReader; import java.io.IOException; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import static java.lang.System.*; public class RunScript { public static void main(String[] args) { if (args.length != 1) { err.println("usage: java RunScript script"); return; } ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("nashorn"); try { engine.eval(new FileReader(args[0])); } catch (ScriptException se) { err.println(se.getMessage()); } catch (IOException ioe) { err.println(ioe.getMessage()); } } }

main()この例のメソッドは、最初に単一のコマンドライン引数(スクリプトファイルの名前)が指定されていることを確認します。それ以外の場合は、使用状況情報を表示してアプリケーションを終了します。

この引数の存在を前提としてmain()javax.script.ScriptEngineManagerクラスをインスタンス化します。ScriptEngineManagerJavaのスクリプトAPIへのエントリポイントです。

次に、ScriptEngineManagerオブジェクトのScriptEngine getEngineByName(String shortName)メソッドが呼び出され、目的のshortName値に対応するスクリプトエンジンが取得されます。Java 10は、に渡すことによって取得されるNashornスクリプトエンジンをサポート"nashorn"getEngineByName()ます。返されたオブジェクトのクラスは、javax.script.ScriptEngineインターフェイスを実装します。

ScriptEngineeval()スクリプトを評価するためのいくつかのメソッドを宣言します。メソッドをmain()呼び出してObject eval(Reader reader)java.io.FileReaderオブジェクト引数からスクリプトを読み取り(java.io.IOExceptionスローされないと仮定して)、スクリプトを評価します。このメソッドは、スクリプトの戻り値を返しますが、これは無視します。また、このメソッドjavax.script.ScriptExceptionは、スクリプトでエラーが発生したときにスローされます。

リスト2を次のようにコンパイルします。

javac RunScript.java

最初のスクリプトを提示した後、このアプリケーションを実行する方法を説明します。

純粋関数を使った関数型プログラミング

A pure function is a functional programming function that depends only on its input arguments and no external state. An impure function is a functional programming function that violates either of these requirements. Because pure functions have no interaction with the outside world (apart from calling other pure functions), a pure function always returns the same result for the same arguments. Pure functions also have no observable side effects.

Can a pure function perform I/O?

If I/O is a side effect, can a pure function perform I/O? The answer is yes. Haskell uses monads to address this problem. See "Pure Functions and I/O" for more about pure functions and I/O.

Pure functions versus impure functions

The JavaScript in Listing 3 contrasts an impure calculatebonus() function with a pure calculatebonus2() function.

Listing 3. Comparing pure vs impure functions (script1.js)

// impure bonus calculation var limit = 100; function calculatebonus(numSales) { return(numSales > limit) ? 0.10 * numSales : 0 } print(calculatebonus(174)) // pure bonus calculation function calculatebonus2(numSales) { return (numSales > 100) ? 0.10 * numSales : 0 } print(calculatebonus2(174))

calculatebonus() is impure because it accesses the external limit variable. In contrast, calculatebonus2() is pure because it obeys both requirements for purity. Run script1.js as follows:

java RunScript script1.js

Here's the output you should observe:

17.400000000000002 17.400000000000002

Suppose calculatebonus2() was refactored to return calculatebonus(numSales). Would calculatebonus2() still be pure? The answer is no: when a pure function invokes an impure function, the "pure function" becomes impure.

When no data dependency exists between pure functions, they can be evaluated in any order without affecting the outcome, making them suitable for parallel execution. This is one of functional programming's benefits.

More about impure functions

Not all functional programming functions need to be pure. As Functional Programming: Pure Functions explains, it is possible (and sometimes desirable) to "separate the pure, functional, value based core of your application from an outer, imperative shell."

Functional programming with higher-order functions

A higher-order function is a mathematical function that receives functions as arguments, returns a function to its caller, or both. One example is calculus's differential operator, d/dx, which returns the derivative of function f.

First-class functions are first-class citizens

Closely related to the mathematical higher-order function concept is the first-class function, which is a functional programming function that takes other functional programming functions as arguments and/or returns a functional programming function. First-class functions are first-class citizens because they can appear wherever other first-class program entities (e.g., numbers) can, including being assigned to a variable or being passed as an argument to or returned from a function.

The JavaScript in Listing 4 demonstrates passing anonymous comparison functions to a first-class sorting function.

Listing 4. Passing anonymous comparison functions (script2.js)

function sort(a, cmp) { for (var pass = 0; pass 
    
      pass; i--) if (cmp(a[i], a[pass]) < 0) { var temp = a[i] a[i] = a[pass] a[pass] = temp } } var a = [22, 91, 3, 45, 64, 67, -1] sort(a, function(i, j) { return i - j; }) a.forEach(function(entry) { print(entry) }) print('\n') sort(a, function(i, j) { return j - i; }) a.forEach(function(entry) { print(entry) }) print('\n') a = ["X", "E", "Q", "A", "P"] sort(a, function(i, j) { return i 
     
       j; }) a.forEach(function(entry) { print(entry) }) print('\n') sort(a, function(i, j) { return i > j ? -1 : i < j; }) a.forEach(function(entry) { print(entry) })
     
    

In this example, the initial sort() call receives an array as its first argument, followed by an anonymous comparison function. When called, the anonymous comparison function executes return i - j; to achieve an ascending sort. By reversing i and j, the second comparison function achieves a descending sort. The third and fourth sort() calls receive anonymous comparison functions that are slightly different in order to properly compare string values.

Run the script2.js example as follows:

java RunScript script2.js

Here's the expected output:

-1 3 22 45 64 67 91 91 67 64 45 22 3 -1 A E P Q X X Q P E A

Filter and map

Functional programming languages typically provide several useful higher-order functions. Two common examples are filter and map.

  • A filter processes a list in some order to produce a new list containing exactly those elements of the original list for which a given predicate (think Boolean expression) returns true.
  • A map applies a given function to each element of a list, returning a list of results in the same order.

JavaScript supports filtering and mapping functionality via the filter() and map() higher-order functions. Listing 5 demonstrates these functions for filtering out odd numbers and mapping numbers to their cubes.

Listing 5. Filtering and mapping (script3.js)

print([1, 2, 3, 4, 5, 6].filter(function(num) { return num % 2 == 0 })) print('\n') print([3, 13, 22].map(function(num) { return num * 3 }))

Run the script3.js example as follows:

java RunScript script3.js

You should observe the following output:

2,4,6 9,39,66

Reduce

Another common higher-order function is reduce, which is more commonly known as a fold. This function reduces a list to a single value.

Listing 6 uses JavaScript's reduce() higher-order function to reduce an array of numbers to a single number, which is then divided by the array's length to obtain an average.

Listing 6. Reducing an array of numbers to a single number (script4.js)

var numbers = [22, 30, 43] print(numbers.reduce(function(acc, curval) { return acc + curval }) / numbers.length)

Run Listing 6's script (in script4.js) as follows:

java RunScript script4.js

You should observe the following output:

31.666666666666668

You might think that the filter, map, and reduce higher-order functions obviate the need for if-else and various looping statements, and you would be right. Their internal implementations take care of decisions and iteration.

A higher-order function uses recursion to achieve iteration. A recursive function invokes itself, allowing an operation to repeat until it reaches a base case. You can also leverage recursion to achieve iteration in your functional code.

Functional programming with lazy evaluation

Another important functional programming feature is lazy evaluation (also known as nonstrict evaluation), which is the deferral of expression evaluation for as long as possible. Lazy evaluation offers several benefits, including these two:

  • Expensive (timewise) calculations can be deferred until they're absolutely necessary.
  • Unbounded collections are possible. They'll keep delivering elements for as long as they're requested to do so.

Lazy evaluation is integral to Haskell. It won't calculate anything (including a function's arguments before the function is called) unless it's strictly necessary to do so.

Java's Streams API capitalizes on lazy evaluation. A stream's intermediate operations (e.g., filter()) are always lazy; they don't do anything until a terminal operation (e.g., forEach()) is executed.

Although lazy evaluation is an important part of functional languages, even many imperative languages provide builtin support for some forms of laziness. For example, most programming languages support short-circuit evaluation in the context of the Boolean AND and OR operators. These operators are lazy, refusing to evaluate their right-hand operands when the left-hand operand is false (AND) or true (OR).

Listing 7 is an example of lazy evaluation in a JavaScript script.

Listing 7. Lazy evaluation in JavaScript (script5.js)

var a = false && expensiveFunction("1") var b = true && expensiveFunction("2") var c = false || expensiveFunction("3") var d = true || expensiveFunction("4") function expensiveFunction(id) { print("expensiveFunction() called with " + id) }

Run the code in script5.js as follows:

java RunScript script5.js

You should observe the following output:

expensiveFunction() called with 2 expensiveFunction() called with 3

Lazy evaluation is often combined with memoization, an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs reoccur.

Because lazy evaluation doesn't work with side effects (such as code that produces exceptions and I/O), imperative languages mainly use eager evaluation (also known as strict evaluation), where an expression is evaluated as soon as it's bound to a variable.

More about lazy evaluation and memoization

A Google search will reveal many useful discussions of lazy evaluation with or without memoization. One example is "Optimizing your JavaScript with functional programming."

Functional programming with closures

First-class functions are associated with the concept of a closure, which is a persistent scope that holds onto local variables even after the code execution has left the block in which the local variables were defined.

Crafting closures

Operationally, a closure is a record that stores a function and its environment. The environment maps each of the function's free variables (variables used locally, but defined in an enclosing scope) with the value or reference to which the variable's name was bound when the closure was created. It lets the function access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

To help clarify this concept, Listing 8 presents a JavaScript script that introduces a simple closure. The script is based on the example presented here.

Listing 8. A simple closure (script6.js)

function add(x) { function partialAdd(y) { return y + x } return partialAdd } var add10 = add(10) var add20 = add(20) print(add10(5)) print(add20(5))

Listing 8 defines a first-class function named add() with a parameter x and a nested function partialAdd(). The nested function partialAdd() has access to x because x is in add()'s lexical scope. Function add() returns a closure that contains a reference to partialAdd() and a copy of the environment around add(), in which x has the value assigned to it in a specific invocation of add().

Because add() returns a value of function type, variables add10 and add20 also have function type. The add10(5) invocation returns 15 because the invocation assigns 5 to parameter y in the call to partialAdd(), using the saved environment for partialAdd() where x is 10. The add20(5) invocation returns 25 because, although it also assigns 5 to y in the call to partialAdd(), it's now using another saved environment for partialAdd() where x is 20. Thus, while add10() and add20() use the same function partialAdd(), the associated environments differ and invoking the closures will bind x to two different values in the two invocations, evaluating the function to two different results.

Run Listing 8's script (in script6.js) as follows:

java RunScript script6.js

You should observe the following output:

15 25

Functional programming with currying

Currying is a way to translate the evaluation of a multi-argument function into the evaluation of an equivalent sequence of single-argument functions. For example, a function takes two arguments: x and y. Currying transforms the function into taking only x and returning a function that takes only y. Currying is related to but is not the same as partial application, which is the process of fixing a number of arguments to a function, producing another function of smaller arity.

Listing 9 presents a JavaScript script that demonstrates currying.

Listing 9. Currying in JavaScript (script7.js)

function multiply(x, y) { return x * y } function curried_multiply(x) { return function(y) { return x * y } } print(multiply(6, 7)) print(curried_multiply(6)(7)) var mul_by_4 = curried_multiply(4) print(mul_by_4(2))

The script presents a noncurried two-argument multiply() function, followed by a first-class curried_multiply() function that receives multiplicand argument x and returns a closure containing a reference to an anonymous function (that receives multiplier argument y) and a copy of the environment around curried_multiply(), in which x has the value assigned to it in an invocation of curried_multiply().

The rest of the script first invokes multiply() with two arguments and prints the result. It then invokes curried_multiply() in two ways:

  • curried_multiply(6)(7) results in curried_multiply(6) executing first. The returned closure executes the anonymous function with the closure's saved x value 6 being multiplied by 7.
  • var mul_by_4 = curried_multiply(4) executes curried_multiply(4) and assigns the closure to mul_by_4. mul_by_4(2) executes the anonymous function with the closure's 4 value and the passed argument 2.

Run Listing 9's script (in script7.js) as follows:

java RunScript script7.js

You should observe the following output:

42 42 8

Why use currying?

In his blog post "Why curry helps," Hugh Jackson observes that "little pieces can be configured and reused with ease, without clutter." Quora's "What are the advantages of currying in functional programming?" describes currying as "a cheap form of dependency injection," that eases the process of mapping/filtering/folding (and higher order functions generally). This Q&A also notes that currying "helps us create abstract functions."

In conclusion

In this tutorial you've learned some basics of functional programming. We've used examples in JavaScript to study five core functional programming techniques, which we'll further explore using Java code in Part 2. In addition to touring Java 8's functional programming capabilities, the second half of this tutorial will help you begin to think functionally, by converting an example of object-oriented Java code to its functional equivalent.

Learn more about functional programming

I found the book Introduction to Functional Programming (Richard Bird and Philip Wadler, Prentice Hall International Series in Computing Science, 1992) helpful in learning the basics of functional programming.

This story, "Functional programming for Java developers, Part 1" was originally published by JavaWorld .