1. 概要

このチュートリアルでは、部分的に適用される関数、関数のカリー化、部分的な関数、および関数全般について説明します。

まず、Scalaの関数と、Scalaコンパイラーがさまざまな種類の関数をどのように処理するかを理解します。

2. 機能

Scalaでは、関数は次のような意味でファーストクラスの値です。

  • 関数に渡され、関数から返されます
  • コンテナに入れて変数に割り当てます
  • 通常の値と同様の方法で入力します
  • ローカルおよび式内で構築

関数は主に、この記事で説明されているように、 defまたはval、を使用して定義されます。 def は毎回評価し、valは1回評価します。

val dummyFunctionVal : Int => Int = {
  println(new Date())
  num => num
}

def dummyFunctionDef(number : Int) : Int = {
  println(new Date())
  number
}

println(dummyFunctionVal(10)) // prints out the date and then the result :10
println(dummyFunctionVal(10)) // doesn't print out the date and just returns 10

println(dummyFunctionDef(10)) // prints out the date and then the result :10
println(dummyFunctionDef(10)) // prints out the date and then the result :10

def で関数を定義するとき、それを「メソッド」と呼びます。 これらは、オブジェクトまたはクラス内で定義する必要があります。 また、定義したクラスインスタンスへの暗黙の参照もあります。 それらは技術的には値ではなく、タイプもありません。

一方、 valで関数を定義する場合、はそれを「関数値」と呼びます。 これらは、組み込みのScalaクラス FunctionN の特殊な形式であり、Nの範囲は0から22です。

これらの関数には、公式ドキュメントに見られるように、compose、などの追加のメソッドが定義されています。

関数の値またはメソッドを作成しようとしたときの違いを見てみましょう。

val getNameLengthVal : String =>  Int = name => name.length
val multiplyByTwoVal : Int => Int = num => num * 2

getNameLengthVal.andThen(multiplyByTwoVal) // compiles

def getNameLengthDef(name : String) : Int = name.length
def multiplyByTwoDef(number : Int) : Int = number * 2

getNameLengthDef.andThen(multiplyByTwoDef) // doesn't compile

メソッドを関数値に変換する例を次に示します。

val getNameLengthDefFnValue = getNameLengthDef _
val multiplyByTwoDefFnValue = multiplyByTwoDef _ 

getNameLengthDefFnValue.andThen(multiplyByTwoDefFnValue) // compiles

変換プロセスは、eta展開とも呼ばれ、メソッドを関数値に変換するScalaの手法です。

3. 部分的に適用された機能

部分的に適用された関数は、その名前が示すように、引数が部分的に適用された関数を記述します。 これにより、一般的な関数からより具体的な関数を作成できるだけでなく、コードの繰り返しを回避することができます。

これが意味するのは、関数を適用するときに、関数によって定義されたすべてのパラメーターの引数を渡すのではなく、一部のパラメーターのみを渡し、残りのパラメーターは空白のままにするということです。

Scalaが返すのは、パラメーターリストに、それぞれの順序で空白のままにされた元の関数のパラメーターのみが含まれる新しい関数です。

関数を部分的に適用すると、常に新しい関数が生成されることを理解することが重要です。 元の関数は、すべての引数が完全に適用された場合にのみ評価されます

関数に引数を部分的に指定すると、Scalaは関数を返し、残りのパラメーターを引数として受け入れます。

3.1. 繰り返しコードの回避

プロトコルとドメインを指定してURLを作成する必要がある例を見てみましょう。 プロトコルとURLを連結するために、この例を簡略化します。

まず、素朴で反復的なアプローチを試してみましょう。

def createUrl(protocol: String, domain : String) : String = {
    s"$protocol$domain"
}

val baeldung = createUrl("https://","www.baeldung.com")
val facebook = createUrl("https://","www.facebook.com")
val twitter = createUrl("https://","www.twitter.com")
val google = createUrl("https://","www.google.com")

このコードを見ると、DRYの原則に違反していることが簡単にわかります。 コード全体で同じプロトコルhttps://を継続的に提供しています。

プロトコルを部分的に適用できます。これにより、ドメインという1つの引数のみを取る新しい関数が得られます。 プロトコル引数の部分適用の結果として返される関数は、関数値です。

その後、変数に割り当てたり、関数に出し入れしたりできるという点で、プログラム内の他の値として扱うことができます。

Scalaで関数を部分的に適用するには、必要な引数を指定し、指定されていない引数に適切なタイプのアンダースコア_を使用します。

def createUrl(protocol: String, domain : String) : String = {
  s"$protocol$domain" 
} 
val withHttpsProtocol: String => String = createUrl("https://", _: String)
val withHttpProtocol: String => String = createUrl("http://", _: String)
val withFtpProtocol: String => String = createUrl("ftp://", _: String)

この例では、元の方法 createUrl 2つの文字列を受け取り、次のように記述された文字列を返すタイプがあると考えることができます。 (文字列、文字列)=>文字列。 単一の引数を部分的に適用することにより、提供されていない引数が1つ残されるため、戻り型が返されます。 文字列=>文字列。 

これで、新しい関数値 withHttpsProtocol を使用して、プロトコル https://を繰り返し指定しなくてもURLを作成できます。

val baeldung = withHttpsProtocol("www.baeldung.com")
val facebook = withHttpsProtocol("www.facebook.com")
val twitter = withHttpsProtocol("www.twitter.com")
val google = withHttpsProtocol("www.google.com")

部分的に適用された関数を使用して繰り返しを回避した方法は明らかです。

3.2. 部分的に適用された関数のバリエーション

より実用的な例を見てみましょう。 この例では、HTMLをフォーマットする関数を記述します。

def htmlPrinter(tag: String, value: String, attributes: Map[String, String]): String = {
  s"<$tag${attributes.map { case (k, v) => s"""$k="$v"""" }.mkString(" ", " ", "")}>$value</$tag>"
}
htmlPrinter("div","Big Animal",Map("size" -> "34","show" -> "true")) 
// prints <div size="34" show="true">Big Animal</div> 

属性付きのHTMLタグを作成できる関数を設計しました。 同じ属性を持ついくつかのHTML要素を記述したい場合は、次のようにすることができます。

val div1 = htmlPrinter("div", "1", Map("size" -> "34")) 
val div2 = htmlPrinter("div", "2", Map("size" -> "34"))
val div3 = htmlPrinter("div", "3", Map("size" -> "34")) 
val div4 = htmlPrinter("div", "4", Map("size" -> "34"))

または、関数を部分的に適用することで繰り返しを回避することもできます。

val withDiv: (String, Map[String, String]) => String = htmlPrinter("div", _:String , _: Map[String,String])
val withDivAndAttr: String => String = withDiv(_:String, Map("size" -> "34"))
val withDivAndAttr2: String => String = htmlPrinter("div", _: String, Map("size" -> "34"))
val withAttr: (String, String) => String = htmlPrinter(_: String, _:String ,  Map("size" -> "34"))
assert(withDivAndAttr("1").contentEquals(htmlPrinter("div","1",Map("size" -> "34")))) // true

withDiv を部分的に適用できる理由は、技術的にはまだ機能であるためです。

いくつかの議論を部分的に適用することにより、ボイラープレートの多くを減らすことができました。 また、単一または複数の引数を部分的に適用できたほか、引数を部分的に適用できるさまざまなバリエーションも確認できます。

すべての引数が完全に指定された場合にのみ、部分的に適用された関数の関数本体が評価されます

4. 機能カリー化

カレー関数は、一度に複数の引数グループをとる関数を非公式に記述します。

3つの引数グループを持つ関数が与えられた場合、カレーバージョンは1つの引数グループを取り、次の引数グループを受け取る関数を返します。次の引数グループは、3番目の引数グループを受け取る関数を返します。

これは、部分的に適用される関数に似ています。 いくつかの引数を指定し、残りの要素を受け入れる関数を取得しました。

カリー化と部分的に適用される関数の概念は似ていますが、技術的には同じではありません。

関数カリー化により、関数(A、B、C、D)を( A =>(B =>(C => D)))に変換できます。

Scalaでは、単一のパラメーターグループではなく、複数のパラメーターグループを使用してカレー関数を定義します。

def dummyFunction(a: Int, b: Int)(c: Int, d: Int)(e: Int, f: Int): Int = {
   a + b + c + d + e + f
 }
  
val first: (Int, Int) => (Int, Int) => Int = dummyFunction(1,2)
val second: (Int, Int) => Int = first(3,4)
val third : Int =  second(5,6)

複数のパラメーターグループを使用して関数を定義しました。各パラメーターグループを指定すると、次のパラメーターグループを受け入れる新しい関数が返されます。

私たちは自分自身に言うかもしれません、これはやり過ぎではありませんか? 単一のパラメーターグループを使用するだけで、角かっこを追加する手間を省くことができます。

Future のScalaドキュメントを見ると、Futureの構築に使用されるapplyメソッドは、2番目のパラメーターグループが暗黙の実行コンテキストの単一の引数。

単一のパラメータグループを持つ未来は、この例で見られるほど美的に満足できるものではありません。

val executionContext: ExecutionContext = ExecutionContext.fromExecutorService(Executors.newCachedThreadPool())
// invalid!
val future1 = Future({ 
  Thread.sleep(1000) 
  2 }, executionContext)

val future2 = Future {
  Thread.sleep(1000) 
  2 
}(executionContext)

future1future2、の違いがわかります。一方はカレーされ、もう一方はカレーされていません。その結果、ブレース内の関数本体だけではないため、ブラケット内のブレースになります。 applyメソッドの引数。

カレー関数が有益なもう1つの領域は、ジェネリックスを使用する場合の型推論です。

foreach メソッドがシーケンスに存在しなかったと想像して、次のようなものを書き込もうとします。

def withListItems[T](list : List[T],f : T => Unit) : Unit = {
   list match { 
     case Nil => () 
     case h :: t => 
       f(h)
       withListItems(t,f) 
   } 
 } 
withListItems(List(1,2,3,4), number => println(number + 2)) // does not compile

最後の式が期待どおりにコンパイルされていないことがわかります。 コンパイラは、タイプ T 整数であり、メソッド+を持っていると自動的に推測することを期待しています。 Scalaコンパイラーはこの方法で型を推測することはできません。

同じ関数を比較してみましょうが、カレーして、コンパイラがどのように反応するかを見てみましょう。

def withListItems[T](list : List[T])(f : T => Unit) : Unit = {
    list match {
      case Nil => ()
      case h :: t =>
        f(h)
        withListItems(t)(f)
    }
  }

withListItems(List(1,2,3,4))(number => println(number + 2)) // compiles

型を推測できるため、最後の式がコンパイルされることがわかります。

コンパイラーは、あるパラメーターの推論を使用して、それらのパラメーターが異なるパラメーター・グループにない限り、別のパラメーターの解決に役立てることはできません。

型推論は左から右に流れますパラメータグループからパラメータグループへ 。 パラメータグループ内の型推論は、すべてのパラメータに対して同時に行われます。

最初の例では、コンパイラーは関数の引数 number as Integer を推測できませんでした。これは、上記で説明したように、Scalaコンパイラーがパラメーターグループ内の単一のパラメーターを使用して推測できないためです。同じパラメータグループ内の別のパラメータのタイプ。

2番目の例では、最初のパラメーターグループが評価し、コンパイラーが変数番号整数として推測できるようにして、タイプTとして識別します。 ]整数。 次に、その情報をコンパイラーが使用して、関数の引数numberが実際にはIntegerであると推測しました。

5. 部分関数

部分的に適用された関数は、可能な入力値のサブセットに対してのみ機能する関数を定義します。 これにより、入力パラメーターに基づいて情報に基づいた決定を行うことができます。

PartialFunction トレイトのインスタンスを作成することにより、Scalaで部分関数を定義します。

val isWorkingAge : PartialFunction[Int,String] = new PartialFunction[Int,String] {
    override def isDefinedAt(x: Int): Boolean = if(x >= 18 && x <= 60) true else false

    override def apply(v1: Int): String = {
      if (isDefinedAt(v1)) {
        s"You are $v1 years old within working age"
      }else{
        s"You are $v1 years old and not within working age"
      }
    }
  }
isWorkingAge(12) // You are 12 years old and not within working age
isWorkingAge(22) // You are 22 years old within working age

部分関数特性は、許容可能な値のサブセットを定義する isDefinedAt、、および関数のロジックが実行されるapplyメソッドの2つのメソッドを提供します。 これにより、コードの実際のロジックから条件ステートメントを分離できます。

知らなくても部分関数を使うことがよくあります。 パターンマッチを行うときはいつでも、私たちが行うcaseステートメントは部分関数です。

前の例は冗長でした。 ケースステートメントを使用して改善してみましょう。

val isWorkingAge : PartialFunction[Int,String] = {
    case age if age >= 18 && age <= 60 => s"You are $age years old and within working age"
    case other => s"You are $other years old and not within working age"
}

isDefinedAt メソッドはどこにあるのでしょうか。この例では、唯一のケースまたは単一のケースを定義し、otherケースを介してキャッチオール句を提供することで巧妙にそれを行いました。声明。 これは、関数が対応していない値のサブセットを表します。

関数値と同様に、部分分数も構成可能性を提供します。 orElse など、比較可能性を実現するのに役立ついくつかのメソッドを提供します。

5.1. 部分関数の構成

一緒にチェーンされた複数の部分関数を使用するように例を変更してみましょう。

val isWorkingAge : PartialFunction[Int,String] = {
  case age if age >= 18 && age <= 60 => s"You are $age years old within working age"
}

val isYoung : PartialFunction[Int,String] = {
  case age if age < 18 => s"You are less than 18, and not within the working age"
}

val isOld : PartialFunction[Int,String] = {
  case age if age > 60 => s"You are greater than 60 and not within the working age"
}

val verdict = isWorkingAge orElse isYoung orElse isOld

verdict(12) // You are less than 18, and not within the working age
verdict(22) // You are 22 years old within working age
verdict(70) // You are greater than 60 and not within the working age

orElse メソッドを使用して、いくつかの部分関数を一緒に作成できました。このメソッドは、現在の部分関数が引数を処理できない場合、またはその特定の引数に対して定義されていない場合に、指定された引数を次の部分関数に渡します。

技術用語では、 orElseは、部分分数で使用される場合、 isDefinedAtメソッドがその特定の引数に対してfalseを返す場合、指定された引数を次の部分分数に渡します。 。

他のプログラミング言語とは異なり、Scalaは部分関数を広範囲に使用します。 これは、シーケンスのcollectメソッドおよびActorsでも使用されます。

6. 結論

この記事では、さまざまなタイプの関数とその使用法について説明しました。 また、これらのタイプの関数を適切に利用して、よりクリーンでエレガントで面倒なコードを記述できないことも確認しました。

コードスニペットと例は、GitHubにあります。