Scalaでの関数型プログラミング入門
1. 概要
Scala は、オブジェクト指向プログラミングと関数型プログラミングの両方を非常に簡潔で高レベルで表現力豊かな言語に組み込んだ、タイプセーフなJVM言語です。 Scalaは、データ処理用のApache Sparkのようなフレームワークと、必要なニーズに基づいてプログラムをスケーリングするためのツールを提供します。 Scalaのこれらすべての強力な機能により、Scalaは非常に実用的で人気のある言語になっています。
このチュートリアルでは、プログラミング言語としてScalaを使用した関数型プログラミングの概念について学習します。
2. 関数型プログラミングとは何ですか?
関数型プログラミングは、関数をプログラムの中心的な構成要素として使用するプログラミングパラダイムです。 関数型プログラミングでは、純粋関数と不変の値を使用するように努めています。
2.1. 不変性
不変性とは、定数を使用してプログラミングすることを意味します。つまり、変数の値や状態を変更することはできません。 同じことがオブジェクトにも当てはまります。新しいオブジェクトを作成することはできますが、既存のオブジェクトの状態を変更することはできません。 したがって、不変オブジェクトは、可変オブジェクトよりもスレッドセーフです。 Scalaがどのように不変性を強制するかについては、後のセクションで学習します。
2.2. 純粋関数
純粋関数には2つの重要な特性があります。
- 同じ入力に対して常に同じ値を返します。
- 副作用はありません。 副作用のない関数は、単に結果を返すだけです。 プログラムの状態と相互作用する関数は、副作用を引き起こす可能性があります。 プログラムの状態は、たとえば、可変オブジェクト、グローバル変数、またはI/O操作にすることができます。
次のセクションでは、Scalaの純粋関数について詳しく説明します。
3. Scalaはどのように機能しますか?
Scalaは、すべての関数が値であるという意味で関数言語です。 関数型Scalaの基本的な概念は、関数がScalaで一級市民として機能することです。
オブジェクト指向プログラミングで作成できるさまざまな関数構造があります。たとえば、相互に定義されている他の関数やメソッドを取得または返す関数などです。 しかし、関数型プログラミングでは、これらは自然に発生し、非常に頻繁に発生します。 オブジェクト指向プログラミングでは、これらの現象に遭遇することはめったにありません。
3.1. 第一級市民として機能する
関数を値として扱う場合、それを第一級関数と見なします。
一般に、第一級関数は次のようになります。
- 変数に割り当てられます
- 他の関数に引数として渡される
- 他の関数から値として返される
Scalaは、デフォルトですべての関数をファーストクラス関数として扱います。
3.2. 高階関数
高階関数(HOF)の概念は、第一級市民としての関数の概念と密接に関連しています。
高階関数には、次のプロパティの少なくとも1つがあります。
- 1つ以上の関数をパラメーターとして受け取ります
- 結果として関数を返します
基本的に、HOFを使用することで、他の種類の値を操作するのと同じように関数を操作できます。
関数をパラメーターとして受け取る関数を作成して、これを理解しましょう。
def calcAnything(number: Int, calcFunction: Int => Int): Int = calcFunction(number)
def calcSquare(num: Int): Int = num * num
def calcCube(num: Int): Int = num * num * num
val squareCalculated = calcAnything(2, calcSquare)
assert(squareCalculated == 4)
val cubeCalculated = .calcAnything(3, calcCube)
assert(cubeCalculated == 27)
結果として別の関数を返す関数のもう1つの例を見てみましょう。
def performAddition(x: Int, y: Int): Int = x + y
def performSubtraction(x: Int, y: Int): Int = x - y
def performMultiplication(x: Int, y: Int): Int = x * y
def performArithmeticOperation(num1: Int, num2: Int, operation: String): Int = {
operation match {
case "addition" => performAddition(num1, num2)
case "subtraction" => performSubtraction(num1, num2)
case "multiplication" => performMultiplication(num1, num2)
case _ => -1
}
}
val additionResult = performArithmeticOperation(2, 4, "addition")
assert(additionResult == 6)
val subtractionResult = performArithmeticOperation(10, 6, "subtraction")
assert(subtractionResult == 4)
val multiplicationResult = performArithmeticOperation(8, 5, "multiplication")
assert(multiplicationResult == 40)
高階関数を使用すると、関数合成、ラムダ関数、または無名関数を作成することもできます。
3.3. 匿名関数
HOFを使用する場合、既存の名前付き関数を提供するよりも、無名関数または関数リテラルをパラメーターとしてこれらの関数に渡すと便利なことがよくあります。 名前はありませんが、本体、入力パラメーター、および戻り型(オプション)を持つ関数は無名関数です。 これをFunctionLiteralまたはLambdaExpressionとも呼びます。
匿名関数を引数として取る古典的なHOFには、Scalaの標準コレクションライブラリの map、filter、fold関数があります。
3.4. クロージャ
クロージャはScalaの他の関数と同じです。 名前付き関数または無名関数の場合もあれば、純粋または不純の場合もありますが、クロージャは主に関数です。
では、関数とクロージャの違いは何でしょうか。 簡単な例でこれを理解しましょう:
val rate = 0.10
val time = 2
def calcSimpleInterest(principal: Double): Double = {
(principal * rate * time) / 100
}
val simpleInterest = 20
assert(calcSimpleInterest(10000) == simpleInterest)
この例では、関数 calcSimpleInterest()を定義しました。この関数は、rateとtimeの2つの自由変数を使用しています。 したがって、これはクロージャです。
自由変数は、関数で使用される変数ですが、ローカル変数でも関数の仮パラメーターでもありません。 関数とクロージャの唯一の違いは、クロージャが1つ以上の自由変数を使用することです。
3.5. カリー化
カリー化とは、複数の引数をとる関数を、それぞれが1つの引数をとる関数の呼び出しのチェーンに変換することを意味します。 各関数は、後続の引数を取る別の関数を返します。
これにより、次のような式になります。
result = f(x)(y)(z)
2つの引数を持つ関数を作成し、それをカレー関数に変換して、これを理解しましょう。
val multiplication: (Int, Int) => Int = (x, y) => x * y
val curriedMultiplication: Int => Int => Int = x => y => x * y
val multiplicationResult = multiplication(3, 5)
val curriedMultiplicationResult = curriedMultiplication(3)(5)
assert(multiplicationResult == curriedMultiplicationResult)
複数の引数を持つ特別なcurriedメソッドを使用して、同じ操作を実行することもできます。
val conciseCurriedMultiplication: Int => Int => Int = multiplication.curried
val conciseCurriedMultiplicationResult = conciseCurriedMultiplication(3)(5)
assert(multiplicationResult == conciseCurriedMultiplicationResult)
Scalaには、複数の引数リストを作成することでカリー化を容易にするための別個の構文があります。
def addition(x: Int, y: Int): Int = x + y
def curriedAddition(x: Int)(y: Int): Int = x + y
val additionResult = addition(8, 4)
val conciseCurriedAdditionResult = curriedAddition(8)(4)
assert(additionResult == conciseCurriedAdditionResult)
3.6. 部分的に適用された機能
関数型プログラミングでは、パラメーターを持つ関数の呼び出しは、パラメーターに「関数を適用する」と表現することもできます。必要なすべてのパラメーターを使用して関数を呼び出すと、関数は次のように完全に適用されます。そのすべてのパラメータ。 ただし、パラメーターのサブセットのみが関数に渡される場合、その関数は部分的に適用された関数と呼ばれます。必要な初期パラメーターが部分的に適用された関数に提供されると、新しい関数が渡す必要のある残りの引数。
これを理解するために、割引後の販売価格を計算する方法を定義する例を見てみましょう。
def calculateSellingPrice(discount: Double, productPrice: Double): Double = {
(1 - discount/100) * productPrice
}
val discountApplied = calculateSellingPrice(25, _)
val sellingPrice = discountApplied(1000)
assert(sellingPrice == 750)
メソッドcalculateSellingPrice()は、2つの引数を取ります。1つは適用される割引で、もう1つは製品価格です。 店主が25% fまたはすべての製品の割引を設定したシナリオを考えてみます。 この場合、 discount パラメーターを関数に渡すだけで、productPriceが欠落しているパラメーターとして新しい関数を返すことができます。 そうすれば、 discount の値を気にすることなく、すべての製品にdiscountAppliedを使用できます。
4. 結論
この記事では、Scalaの関数型プログラミングを紹介し、Scalaがどのように機能するのか、そしてなぜそれを使用するのかを説明しました。
Scalaは、私たちが慣れ親しんだように感じるのに十分なオブジェクト指向プログラミングを提供してくれます。 同時に、関数型プログラミングを自分のペースで知るための優れた方法です。
いつものように、GitHubでコードスニペットを見つけることができます。