Scalaの関数とメソッド
1. 序章
Scalaの関数とメソッドは同様の概念を表していますが、それらの使用方法には大きな違いがあります。
このチュートリアルでは、それらの定義と使用法の違いを見ていきます。
2. 機能
関数は、単一のパラメーター、パラメーターのリスト、またはパラメーターをまったく受け取らないコードの呼び出し可能な単位です。 関数は、1つまたは複数のステートメントを実行でき、値、値のリスト、または値をまったく返さない場合があります。
このコード単位を再利用できるので、繰り返す必要はありません。
2.1. 匿名関数
匿名関数は、名前または明示的なハンドルのない関数です。 これは、別の関数のパラメーターとしてコードまたはアクションを提供する必要がある場合に役立ちます。
例を見てみましょう:
(number: Int) => number + 1
左側の括弧内に、関数パラメーターを定義します。 => 記号の後に、式またはステートメントのリストを定義します。
パラメータリストは空()、にすることも、必要な数のパラメータを定義することもできます。
() => scala.util.Random.nextInt
(x: Int, y: Int) => (x + 1, y + 1)
複数のステートメントを定義するには、それらを中括弧で囲む必要があります。 コードブロック内の最終式の結果は、関数の戻り値です。
Scalaにはreturnキーワードがありますが、めったに使用されません。
(number: Int) => {
println("We are in a function")
number + 1
}
上記の例では、numberという名前の1つのパラメーターを受け取る関数を定義しました。 関数本体には2つの式があります。 最後の式は、パラメーター値を1つ増やし、結果を返します。
結果のタイプは自動的に推測されます—引数 numberはタイプInt、であり、1を加算すると、結果もInt。になります。
2.2. 名前付き関数
すべてがScalaのオブジェクトであるため、値に関数を割り当てることができます:
val inc = (number: Int) => number + 1
値incに関数が含まれるようになりました。 この値は、関数で定義されたコードの単位を呼び出す必要があるすべての場所で使用できます。
scala> println(inc(10))
11
したがって、incは名前付き関数と見なされます。
関数を変数に割り当てるためにScalaが行う舞台裏の「魔法」がいくつかあります。 Scalaのすべての関数は、Function1またはFunction2またはFunctionN 、タイプの特殊なオブジェクトです。ここで、Nは関数への入力引数の数です。
コンパイラは、提供した機能コードを適切なオブジェクトに自動的にボックス化します。 このオブジェクトには、関数を実行するために呼び出すことができるメソッド apply()が含まれています。
さらに、括弧は関数の apply()メソッドを呼び出すための「シンタックスシュガー」です。 したがって、これら2つのステートメントは同等です。
scala> println(inc(10))
11
scala> println(inc.apply(10))
11
2.3. 閉鎖
戻り値がコンテキスト外の状態に依存する関数は、クロージャと呼ばれます。 クロージャーは、開発者が特定のコンテキストで閉じられた関数を渡すことができるため、オブジェクトをエミュレートする純粋関数メカニズムです。
これを説明するために、プロッタ機能があると仮定しましょう。
def plot(f: Double => Double) = { // Some implementation }
私たちの関数plotは、任意の関数 f(x)を取り、それを視覚化します。
次に、2次元の一次方程式の定義を示します。
val lines: (Double, Double, Double) => Double = (a,b,x) => a*x + b
この方程式は、2Dで可能なすべての線を表します。 たとえばプロット用に1つの特定の線を取得するには、aおよびb係数を指定する必要があります。 xのみが線形方程式の可変部分である必要があります。
lines関数をそのままプロッタ機能で使用することはできません。 plot 関数は、引数として、Double型とDouble型のパラメーターを1つだけ返す別の関数を戻り値として受け取ります。 閉鎖はそれを助けることができます:
val line: (Double, Double) => Double => Double = (a,b) => x => lines(a,b,x)
line 関数は高階関数であり、 Double => Double タイプの別の関数を返します。これは、
val a45DegreeLine = line(1,0)
関数a45DegreeLine()は、 X 座標の値を取り、対応する Y 座標を返し、これらの座標の中心を45度で通る線を定義します。 。
これで、プロット機能を使用して線を引くことができます。
plot(a45DegreeLine())
3. メソッド
メソッドは基本的に、クラス構造の一部であり、オーバーライドでき、異なる構文を使用する関数です。 Scalaでは匿名メソッドを定義することはできません。
メソッド定義には、特別なキーワードdefを使用する必要があります。
def inc(number: Int): Int = number + 1
メソッドの名前の後の括弧内に、メソッドのパラメーターのリストを定義できます。 パラメータリストの後に、先頭にコロン記号が付いたメソッドの戻り型を指定できます。
次に、等号の後にメソッドの本体を定義します。 コードのステートメントが1つしかない場合、コンパイラーでは中括弧をスキップできます。
メソッド定義を使用すると、パラメーターのないメソッドの括弧を完全にスキップできます。 これを示すために、同じ実装のメソッドと関数を提供しましょう。
def randomIntMethod(): Int = scala.util.Random.nextInt
val randomIntFunction = () => scala.util.Random.nextInt
メソッド名を書くときはいつでも、コンパイラーはそれをこのメソッドの呼び出しと見なします。 対照的に、括弧のない関数名は、関数オブジェクトへの参照を保持する単なる変数です。
scala> println(randomIntMethod)
1811098264
scala> println(randomIntFunction)
$line12.$read$$iw$$iw..$$iw$$iw$$$Lambda$4008/0x00000008015cac40@767ee25d
scala> println(randomIntFunction())
1292055875
したがって、メソッドを関数に変換するためにが必要な場合は、アンダースコアを使用できます。
val incFunction = inc _
変数incFunctionには関数が含まれており、定義した他の関数として使用できます。
scala> println(incFunction(32))
33
関数をメソッドに変換する対応する方法はありません。
3.1. ネストされたメソッド
Scalaの別のメソッド内でメソッドを定義できます。
def outerMethod() = {
def innerMethod() = {
// inner method's statements here
}
// outer method's statements
innerMethod()
}
あるメソッドが別のメソッドのコンテキストと緊密に結合されている状況で、ネスト機能を使用できます。 ネストを使用すると、コードはより整理されたように見えます。
階乗計算の再帰的実装によるネストされたメソッドの一般的な使用法を見てみましょう。
import scala.annotation.tailrec
def factorial(num: Int): Int = {
@tailrec
def fact(num: Int, acc: Int): Int = {
if (num == 0)
acc
else
fact(num - 1, acc * num)
}
fact(num, 1)
}
再帰的実装では、再帰的呼び出し間の一時的な結果を累積するために追加のメソッドパラメーターが必要です。そのため、シンプルなインターフェイスを備えた装飾的な外部メソッドを使用して、実装を非表示にすることができます。
3.2. パラメータ化
Scalaでは、タイプごとにメソッドをパラメーター化できます。 パラメータ化により、再利用可能なコードを使用して汎用メソッドを作成できます。
def pop[T](seq: Seq[T]): T = seq.head
ご覧のとおり、typeパラメーターはメソッド宣言中に角括弧で囲まれて提供されました。
これで、メソッドpopを任意のタイプTに使用して、関数パラメーターとして提供されたシーケンスの先頭を取得できるようになりました。
簡単に言えば、pop関数を一般化しました。
scala> val strings = Seq("a", "b", "c")
scala> val first = pop(strings)
first: String = a
scala> val ints = Seq(10, 3, 11, 22, 10)
scala> val second = pop(ints)
second: Int = 10
この場合、コンパイラはタイプを推測するため、対応する値は適切なタイプになります。 値firstはタイプString、になり、値secondはタイプInt。になります。
3.3. 拡張方法
Scalaのimplicit機能により、既存のタイプに追加機能を提供できます。
メソッドを使用して新しいタイプを定義し、新しいタイプから既存のタイプへの暗黙的な変換を提供する必要があります。
implicit class IntExtension(val value: Int) extends AnyVal {
def isOdd = value % 2 == 0
}
ここでは、いわゆる値クラスを定義しました。 値クラスは、特定のタイプの任意の値の単なるラッパーです。 この例では、値のタイプはIntです。
値クラスは、クラスであり、パラメーターが1つだけで、その本体内にメソッドのみがあります。 AnyVal タイプから拡張し、不要なオブジェクトの割り当てを回避します。
暗黙の変換を定義するには、クラスを暗黙のとしてマークする必要があります。 これにより、クラス内のすべてのメソッドに暗黙的にアクセスできるようになります。
このようなクラスを現在のコンテキストにインポートすると、値型自体で定義されているため、このクラス内で定義されているすべてのメソッドを使用できます。
scala> 10.isOdd
res1: Boolean = true
scala> 11.isOdd
res2: Boolean = false
Int値10には明示的なメソッドisOdd はありませんが、IntExtensionクラスには明示的なメソッドがあります。 コンパイラは、IntからIntExtensionへのimplicit変換を検索します。 私たちのバリュークラスはそのような変換を提供します。 コンパイラーはこれを使用して、必要なパラメーターを使用して関数isOddの呼び出しを解決します。
4. 名前によるパラメータ
これまで、「値による」パラメータを使用してきました。 つまり、関数またはメソッド内のパラメーターは、その値にアクセスする前に評価されます。
def byValue(num: Int) = {
println(s"First access: $num. Second access: $num")
}
scala> byValue(scala.util.Random.nextInt)
First access: 1705836657. Second access: 1705836657
byValue 関数の呼び出しのパラメーターとして、関数scala.util.Random.nextIntを提供しました。 予想どおり、パラメーターの値は、println関数で使用する前に評価されます。 パラメータへの両方のアクセスは同じ値を提供します。
Scalaは「名前による」パラメーターもサポートしています。 「名前による」パラメーターは、関数内でそれらにアクセスするたびに評価されます。
「名前による」パラメータを定義するには、そのタイプの前に => (矢印記号)を付ける必要があります。
def byName(num: => Int) = {
println(s"First access: $num. Second access: $num")
}
scala> byName(scala.util.Random.nextInt)
First access: 1349966645. Second access: 236297674
関数の呼び出しは同じですが、結果は少し異なります。 「名前で」提供されたパラメーターにアクセスするたびに、パラメーターの値を再度評価します。 そのため、アクセスごとに異なる結果が得られました。
5. 結論
このチュートリアルでは、Scalaで関数とメソッドを定義する方法と、それらの使用法の主な違いと共通点を示しました。 クロージャーの使用法を示し、既存のタイプに拡張メソッドを提供する方法を学びました。
いつものように、例はGitHubのにあります。