1. 序章

このチュートリアルでは、関数型プログラミング用のScalaライブラリであるScalazの機能について説明します。 Scalazには、純粋に機能的なデータ構造と型クラスが付属しています。

2. 依存関係

まず、build.sbtにScalazライブラリを追加しましょう。

libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.3.2"

3. 等しい

Scalaは、任意の2つの値が等しいかどうかを比較するための二重等号演算子( == )を提供します。 ただし、型安全性の欠如があります。 異なるタイプの値を比較すると、常にfalseが生成されます。 幸い、Scalazは、必要な型安全性を追加するためのトリプル等号演算子(===)を提供しています。 === を使用してさまざまなタイプの値を比較すると、コンパイル時に失敗します。

Type Mismatch.
Required: Int
Found: String

これにより、コードについて考え、場合によってはアプローチを変更する必要があります。 同様に、Scalazは、Scalaの!=の代わりに使用する不等式チェック演算子=/ = も提供します。これは、同様にタイプセーフではありません。

assertTrue(15 === 15)
assertTrue(15 =/= 25)

4. 注文

Order 型クラスであり、値を比較するための豊富な演算子のセットを提供します。 通常、何かをソート可能または順序付けにする場合は、 scala.math.Ordering を拡張し、compareメソッドをオーバーライドします。 の強みの1つ注文以上注文のような便利なオペレーターの存在です < >> > = 、 と <= 。 ただし、これらはscala.math.Orderedトレイトにもあります。 では、ScalaがOrderingOrderedで出荷されるときに、なぜ Order を気にする必要があるのでしょうか。これにより、目的を達成できます。 明らかな利点の1つは、Scala演算子がタイプセーフではないことです。 この呼び出しは、Scalaで有効な間、Scalazでは失敗します。

3 < 5.6

さらに、コードにアクセスできないサードパーティのクラスに比較動作を追加したい場合があります。 この場合、そのクラスをOrderingまたはOrderedに拡張するオプションはありません。 ただし、 implicits を使用することで、この動作を任意のクラスに追加できます。 学生のスコアをラップするScoreというドメインオブジェクトを扱っていると仮定します。

case class Score(amt: Double)

Score の1つのインスタンスが別のインスタンスと比較可能であることを確認したいので、implicitオブジェクトを作成して順序付けを有効にします。

implicit object scoreOrdered extends Order[Score] {
  override def order(x: Score, y: Score): Ordering =
    x.amt compare y.amt match {
      case 1  => Ordering.GT
      case 0  => Ordering.EQ
      case -1 => Ordering.LT
    }
}

結果として、この暗黙のオブジェクトが表示されている場合は常に、さまざまな操作のセットを適用して、タイプScoreの値を比較できます。

assertTrue(Score(0.9) > Score(0.8))
assertTrue(Score(0.9) gt Score(0.8))
assertTrue(Score(0.9) gte Score(0.8))
assertFalse(Score(0.9) < Score(0.8))
assertFalse(Score(0.9) lt Score(0.8))
assertFalse(Score(0.9) lte Score(0.8))

さらに、scalaz.Orderingのインスタンスとして順序関係を取得することもできます。

assertEquals(Ordering.GT, Score(0.9) ?|? Score(0.8))
assertEquals(Ordering.LT, Score(0.8) ?|? Score(0.9))
assertEquals(Ordering.EQ, Score(0.9) ?|? Score(0.9))

5. 表示

Showの主な目的は、文字列表現のイディオムを提供することです toString メソッドと同じように機能しますが、サードパーティライブラリの一部のクラスはfinalであるため、toStringメソッドをオーバーライドできません。 implicits の概念と組み合わせると、 Show は、toStringが不足している場合に非常に便利です。 Scalazは、Int、Double、Floatなどの標準データ型に対して暗黙のShowを提供します。 オブジェクトでメソッドshowを呼び出すと、 Cord が返されます。これは、非常に長い可能性のあるStringsを効率的に格納および操作するための純粋に機能的なデータ構造です。 Show の実装には、Cordの代わりに実際のString表現を返すshowsという別の便利なメソッドが含まれています。

assertEquals("3", 3.shows)
assertEquals("3.4", 3.4.shows)

最後に、カスタムを作成するには見せる実装では、ターゲットクラスの暗黙的なオブジェクトを作成して拡張できます表示[クラス> ]

implicit object threadShow extends Show[Thread] {
  override def show(f: Thread): Cord =
    Cord(s"Thread id = ${f.getId}, name = ${f.getName}")
}

6. 列挙型

Enum は、変数の値を有界型に設定できるようにするデータ型です。 これは、変数には、事前定義された値のセットの1つのみを一度に含めることができることを意味します。 たとえば、アルファベットの文字を表す変数には、AからZまでの値のみを含めることができますが、曜日の1つには、MONDAYからまでの1つのみを含めることができます。 日曜日。 Scalazは、列挙型クラスの列挙型を処理するための強力な演算子を提供します。 文字の範囲が与えられると、それから列挙型を簡単に生成できます。 さらに、ユースケースに適している場合は、Enumを標準のScalaリストに変換できます。

val enum = 'a' |-> 'g'
val enumAsList = enum.toList

val expectedResult = IList('a', 'b', 'c', 'd', 'e', 'f', 'g')
val expectedResultAsList = List('a', 'b', 'c', 'd', 'e', 'f', 'g')

assertEquals(expectedResult, enum)
assertEquals(expectedResultAsList, enumAsList)

開始点も単一のEnum値である場合、.predおよび.succを使用して、それぞれ前の値または次の値を取得できます。

assertEquals('c', 'b'.succ)
assertEquals('a', 'b'.pred)

提供された値よりもnステップ進んだEnum値を取得することもできます。

assertEquals('e', 'b' -+- 3)
assertEquals('f', 'b' -+- 4)

または、nステップ遅れているものを取得できます。

assertEquals('b', 'e' --- 3)
assertEquals('a', 'c' --- 2)

列挙型の最小値と最大値を取得することもできます。 Scalazは、IntDoubleなどの標準の境界タイプのEnumビューを取得できます。 結果はOptionであるため、最小値と最大値を計算できない場合は、空の値を取得します。

assertEquals(Some(-2147483648), Enum[Int].min)
assertEquals(Some(2147483647), Enum[Int].max)

minおよびmaxを正常に使用するには、Enumでこれらが定義されている必要があります。 前に見たようにEnumをステップスルーするには、Orderingが実装されている必要があります。 作業中のチケットに割り当てることができるさまざまな優先度を反映するために、優先度Enumを作成するとします。 まず、優先度オブジェクトのケースクラスを作成します。

case class Priority(num: Int, name: String)

次に、いくつかの既知の優先度インスタンスを作成しましょう。

val HIGH = Priority(1, "HIGH") 
val MEDIUM = Priority(2, "MEDIUM") 
val LOW = Priority(3, "LOW")

最後に、Enumを拡張する暗黙のオブジェクトを作成します。

implicit object PriorityEnum extends Enum[Priority] {
  def order(p1: Priority, p2: Priority): Ordering =
    (p1.num compare p2.num) match {
      case -1 => Ordering.LT
      case 0  => Ordering.EQ
      case 1  => Ordering.GT
    }

  def succ(s: Priority): Priority = s match {
    case LOW    => MEDIUM
    case MEDIUM => HIGH
    case HIGH   => LOW
  }

  def pred(s: Priority): Priority = s match {
    case LOW    => HIGH
    case MEDIUM => LOW
    case HIGH   => MEDIUM
  }

  override def max = Some(HIGH)
  override def min = Some(LOW)
}

これで、以前のすべてのテストを新しい列挙型で実行できます。

// generate range
val expectedEnum = IList(Priority(1, "LOW"), Priority(2, "MEDIUM"), Priority(3, "HIGH"))
assertEquals(expectedEnum, LOW |-> HIGH)

//range to list
assertEquals(expectedEnum.toList, (LOW |-> HIGH).toList)

//pred and succ
assertEquals(HIGH, LOW.pred)
assertEquals(HIGH, MEDIUM.succ)

//step forward and back
assertEquals(MEDIUM, HIGH -+- 2)
assertEquals(LOW, LOW --- 3)

//min and max
assertEquals(Some(Priority(3, "HIGH")), Enum[Priority].max)
assertEquals(Some(Priority(1, "LOW")), Enum[Priority].min)

7. オプション操作

Scalazには、オプション操作用の数の新しい構成が付属しています。 ある程度、それらの多くは生活を楽にします。 いくつか見てみましょう。 Scalazのsomeコンストラクトを使用してOptionを作成します。これにより、ScalaのSomeapplyメソッドが内部的に呼び出されます。の大文字と小文字の違いに注意してください。 ] S in SomeおよびN in None

assertEquals(Some(12), some(12))
assertEquals(None, none[Int])

または、値に対して some を直接呼び出すことにより、Optionを作成することもできます。

assertEquals(Some(13), 13.some)
assertEquals(Some("baeldung"), "baeldung".some)

さらに、オプション値の抽出に使用する操作があります。 最も一般的なものの1つは、 some /none演算子です。 getOrElse のように機能しますが、値が存在する場合、演算子のsome部分に渡された関数を実行します。 none 部分は、フォールバック値のみを返します。

val opt = some(12)
val value1 = opt some { a =>
  a
} none 0

val value2 = opt
  .some(_ * 2)
  .none(0)

assertEquals(12, value1)
assertEquals(24, value2)

Option 値を抽出するための別の演算子は、パイプ演算子です。 これには2つのオペランドが必要です。左側はOptionで、右側はOptionが空の場合に返されるデフォルト値です。

val opt = some(12)
val opt2 = none[Int]

assertEquals(12, opt | 0)
assertEquals(5, opt2 | 5)

最後に見る演算子は単項()演算子です。 Option が存在する場合はその値を抽出し、存在しない場合はタイプのゼロ値を抽出します。 たとえば、整数の場合、ゼロ値は 0 であり、文字列の場合、それは空の文字列“”です。

assertEquals(25, ~some(25))
assertEquals(0, ~none[Int])
assertEquals("baeldung", ~some("baeldung"))
assertEquals("", ~none[String])

8. 文字列操作

Scalazは、文字列を便利に操作するための演算子をいくつか提供しています。 本当に際立っているのはplural演算子です。 与えられた単語の複数形を返します。

assertEquals("apples", "apple".plural(2))
assertEquals("tries", "try".plural(2))
assertEquals("range rovers", "range rover".plural(2))

9. ブール操作

ブール型を操作するための便利なユーティリティも多数利用できます。 ブール値をfoldする方法を見て、ブール値がtrueまたはfalseのどちらであるかに基づいて、任意のタイプの2つの値のいずれかを選択します。

val t = true
val f = false

val expectedValueOnTrue = "it was true"
val expectedValueOnFalse = "it was false"

val actualValueOnTrue = t.fold[String](expectedValueOnTrue, expectedValueOnFalse)
val actualValueOnFalse = f.fold[String](expectedValueOnTrue, expectedValueOnFalse)

assertEquals(expectedValueOnTrue, actualValueOnTrue)
assertEquals(expectedValueOnFalse, actualValueOnFalse)

option 演算子を使用すると、ブールフラグに基づいてOptionとして値を返すことができます。

val restrictedData = "Some restricted data"

val actualValueOnTrue = true option restrictedData
val actualValueOnFalse = false option restrictedData

assertEquals(Some(restrictedData), actualValueOnTrue)
assertEquals(None, actualValueOnFalse)

ブール値でのoption演算子の実用的な使用法の1つは、ログインしたユーザーの特権レベルに応じて、一部のフィールドを返すか非表示にする認証システムです。 Javaのバックグラウンドを持ついくつかのScala開発者は、3値演算子を使用したインラインifステートメントを見逃しています。 Scalazでも同様のものが利用できます。

val t = true
val f = false

assertEquals("true", t ? "true" | "false")
assertEquals("false", f ? "true" | "false")

特定の条件がtrueである場合、またはデフォルトで処理しているデータ型のゼロ値になっている場合に、値を返したい場合があります。 Scalazは、これを行うための非常に簡潔な方法を提供します。

val t = true
val f = false

assertEquals("string value", t ?? "string value")
assertEquals("", f ?? "string value")

assertEquals(List(1, 2, 3), t ?? List(1, 2, 3))
assertEquals(List(), f ?? List(1, 2, 3))

assertEquals(5, t ?? 5)
assertEquals(0, f ?? 5)

上記の演算子の逆は!?であり、使用すると、条件が false と評価されたときに値を返し、true[のときに型のゼロ値を返します。 X177X]。

10. マップ操作

Map は、私たちの日常のプログラミングで非常に人気のあるデータ構造です。 したがって、Scalazが標準のScalaで利用できるものを超えてマップを処理するためのいくつかの豊富な機能を備えていることは驚くことではありません。

10.1. alter

alterは、キーを最初のパラメーターグループの単一パラメーターとして受け取り、2番目のパラメーターグループの無名関数として受け取る高階関数です。 Mapgetを呼び出した結果を、Optionとして無名関数に渡します。 渡されたキーの新しい値を作成する無名関数でコードを記述できます。全体的な結果は、そのキーの値が変更された新しいMapになります。

val map = Map("a" -> 1, "b" -> 2)

val mapAfterAlter1 = map.alter("b") { maybeValue =>
  maybeValue
    .some(v => some(v * 10))
    .none(some(0))
}
val mapAfterAlter2 = map.alter("c") { maybeValue =>
  maybeValue
    .some(v => some(v * 10))
    .none(some(3))
}

assertEquals(Map("a" -> 1, "b" -> 20), mapAfterAlter1)
assertEquals(Map("a" -> 1, "b" -> 2, "c" -> 3), mapAfterAlter2)

10.2. 交差点

intersectWith関数は2つのマップを取り、それらの交差点である新しいマップを返します。 さらに、無名関数を使用する高階関数でもあります。 交差するキーの値に関数を順番に適用し、結果が交差するキーの最終値として設定されます。

val m1 = Map("a" -> 1, "b" -> 2)
val m2 = Map("b" -> 2, "c" -> 3)
val m3 = Map("a" -> 5, "b" -> 8)

assertEquals(Map("b" -> 4), m1.intersectWith(m2)(_ + _))
assertEquals(Map("b" -> 4), m1.intersectWith(m2)((v1, v2) => v1 * v2))
assertEquals(Map("a" -> -4, "b" -> -6), m1.intersectWith(m3)(_ - _))

10.3. mapKeys

マップ内のキーをマップし、提供したロジックに従って更新する機能もあります。

val m1 = Map("a" -> 1, "b" -> 2)

assertEquals(Map("A" -> 1, "B" -> 2), m1.mapKeys(_.toUpperCase))

10.4. 連合

キーで2つのマップの結合を取得することもでき、キーが重なっている場合は、それらに関数が適用されます。 この関数の結果は、新しいMapのそのキーの値になります。

val m1 = Map("a" -> 1, "b" -> 2)
val m2 = Map("b" -> 2, "c" -> 3)

assertEquals(Map("a" -> 1, "b" -> 4, "c" -> 3), m1.unionWith(m2)(_ + _))

10.5. insertWith

最後に、 insertWith関数を使用すると、キーと値のペアをマップに挿入でき、関数も使用できます。 この関数は、重複するキーを処理するだけです。 標準の場合のように既存の値を上書きする代わりに、2つの値を取得してそれらに関数を適用し、結果を競合するキーの新しい値にします。

val m1 = Map("a" -> 1, "b" -> 2)

val insertResult1 = m1.insertWith("a", 99)(_ + _)
val insertResult2 = m1.insertWith("c", 99)(_ + _)

val expectedResult1 = Map("a" -> 100, "b" -> 2)
val expectedResult2 = Map("a" -> 1, "b" -> 2, "c" -> 99)

assertEquals(expectedResult1, insertResult1)
assertEquals(expectedResult2, insertResult2)

11. NonEmptyList

私たちが使用する最も一般的なデータ構造の1つは、scala.collection.immutable.List。リストの問題の1つは、一部のシナリオで使用する前に、リストが空でないかどうかを確認する必要があることです。 リストの先頭を取得したいとします。

assertEquals(1, List(1).head)

空でないチェックを必要とする一般的なエラーの1つは、空のリストheadを呼び出す場合です。

@Test(expected = classOf[NoSuchElementException])
def givenEmptyList_whenFetchingHead_thenThrowsException(): Unit = {
  List().head
}

NonEmptyList の重要なポイントは、処理しているリストが空にならないことを保証することです。したがって、使用する前に追加のチェックを追加する必要はありません。 これは、オプションでラップされたときに値がnullにならないという保証に似ています。 NonEmptyListsを作成するいくつかの方法を見てみましょう。

//wrap a value in a non-empty list
val nel1 = 1.wrapNel
assertEquals(NonEmptyList(1), nel1)

//standard apply
val nel2 = NonEmptyList(3, 4)

//cons approach
val nel3 = 2 <:: nel2

assertEquals(NonEmptyList(2, 3, 4), nel3)

//append
val nel4 = nel1 append nel3
assertEquals(NonEmptyList(1, 2, 3, 4), nel4)

NonEmptyListListでもあるため、すべての標準のListメソッドも同様に適用されます。

12. レンズ

Lens は、が複雑な不変のネストされたケースクラスを更新するという一般的な問題を解決するのに役立つ関数型プログラミングの抽象化です。 Webアプリケーションにいくつかのドメインオブジェクトを持つユーザーアカウントモジュールがあるとします。

case class UserId(id: Long)
case class FullName(fname: String, lname: String)
case class User(id: UserId, name: FullName)

場合によっては、1つ以上のドメインオブジェクトの値を変更したいことがあります。 不変の構造を扱っているため、ここでは「変更」という言葉を大まかに使用しています。これらの操作により、実際には新しいオブジェクトが生成されます。 標準のScalaを使用して、 Usernameフィールドを変更するには:

val name = FullName(fname = "John", lname = "Doe")
val userId = UserId(10)
val user = User(userId, name)

val updatedName = FullName("Marcus", "Aurelius")
val actual = user.copy(name = updatedName)

assertEquals(User(userId, updatedName), actual)

これはかなり簡潔です。 しかし、前述したように、レンズの力は、オブジェクトが深くネストされているときに見られます。 Userオブジェクトのnameフィールドの名のみを変更してみましょう。

val updatedName = FullName("Jane", "Doe")
val actual = user.copy(name = name.copy(fname = "Jane"))

assertEquals(User(userId, updatedName), actual)

ネストがさらに深くなると、次のような更新コードが生成される可能性があります。

user.copy(name = name.copy(fname = fname.copy(xxx)))

レンズを使用すると、最初のレベルであるかのように、ネストの任意のレベルでフィールドを更新できます。 更新するオブジェクトのレンズを作成して、前の例を書き直してみましょう。

val userFullName = Lens.lensu[User, FullName](
    (user, name) => user.copy(name = name),
    _.name
  )

上記のLensは、Usernameオブジェクト用です。 最初は親オブジェクトタイプで、次はオブジェクトにアクセスする子のタイプの2つのタイプを取ります。 また、そのオブジェクトのセッターとゲッターも必要です。 上記のLensのスコープで、前に見た更新コードを変更できます。

val updatedName = FullName("Marcus", "Aurelius")
val actual = userFullName.set(user, updatedName)

assertEquals(User(userId, updatedName), actual)

ネストされたフィールドを更新するには、ターゲットフィールドに到達するまで、ネストのすべてのレベルでレンズをチェーンする必要があります。 この例では、userオブジェクトのnameオブジェクトのfnameフィールドを更新します。 これには、userからnameまでのレンズがあり、nameからfnameまでの別のレンズにチェーンする必要があります。 ]。 すでにuser-> name Lens があるので、 name -> fnameを作成するだけで済みます。 ]レンズ:

val firstName = Lens.lensu[FullName, String](
    (fullName, firstName) => fullName.copy(fname = firstName),
    _.fname
  )

この時点で、Scalazは、composeや andThen など、使用できるいくつかの演算子を提供します。 それらはエイリアス化されています <= < >=> それぞれ。 user-> fnameLensを見てみましょう。

val userFirstName = userFullName >=> firstName

これは、userを介してnameオブジェクトに更新し、次にnameからfirstNameフィールドに更新することを意味します。

val name = FullName(fname = "John", lname = "Doe")
val userId = UserId(10)
val user = User(userId, name)

val updatedName = FullName("Jane", "Doe")
val actual = userFirstName.set(user, "Jane")

assertEquals(User(userId, updatedName), actual)

新しいLensのスコープにより、ネストされた fname フィールドを、userの第1レベルの子であるかのように更新できるようになりました。 。

13. 結論

この記事では、Scalazライブラリの機能について説明しました。 いつものように、すべてのソースコードはGithub利用できます。