初めに
NonEmptyList
とは...値を1つ以上持っていることが保証されているデータ型です。後続処理で「Listが空だったら」という考慮をする必要がありません。
Scala with CatsのNonEmptyList
について公式ドキュメントを自分なりに翻訳して説明していきます!!
※ところどころ、分かりやすいように文を付け加えてます!
モチベーション
NonEmptyList
の2つの例から見ていきましょう!
Validated
およびIor
での使用
Validated
(公式ドキュメント)やIor
(公式ドキュメント)を見た場合、一般的なケースであれば、これらのデータ構造の1つでNonEmptyList
が使用されているでしょう。
なぜかというと、エラー報告のときに都合がいいからです。名前から分かるようにNonEmptyList
は少なくとも1つの要素を持つという特殊なデータ型です。(よって、後続処理で「Listが空だったら」という考慮をする必要がありません。)
それ以外は通常のList
のように動作します。Validated
(およびIor
)のようなsum type(値が同じであっても、各値が元のタイプのどれからのものであるかを追跡する複数のタイプの結合を表す型)の場合、エラー無しでInvalid
を持つことは意味がありません。エラーが無いということは、それはValid
であるということを意味します!
NonEmptyList
を使用することにより、次のようなことが型で明確に分かります。
Invalid
の場合、少なくとも1つエラーがあります。
これだとより正確になり、後でエラーを報告するときにエラーのListが空である可能性を疑う必要がなくなります。
より具体的な引数の要求によってOption
を回避する
関数型プログラマーである私たちは当然、List
などの有名なhead
メソッドのような例外をスローする可能性のある部分関数を避けます。
平均を計算する関数を例として取り上げましょう。
def average(xs: List[Int]): Double = {
xs.sum / xs.length.toDouble
}
0による除算は例外をスローするため、これは明らかに空のListにとって有効な定義ではないです。これを修正する1つの方法は、Double
ではなくOption
を返すことです。
def average(xs: List[Int]): Option[Double] = if (xs.isEmpty) {
None
} else {
Some(xs.sum / xs.length.toDouble)
}
これだと機能しますし安全です。しかし、これは無効な入力を受け入れている問題を隠すだけです。
Option
を使用することにより、空のListを処理するロジックでaverage
関数を拡張しています。さらに、この関数の呼び出し元は毎回Option
の処理をする必要があります。
例外を見落として失敗するよりはましですが、完璧には程遠いです。
代わりに表したいのは、average
関数は空のListには全く意味が無いということです。ラッキーなことに、CatsではNonEmptyList
を定義しています!これは構造上、空にすることができないListを表しています。つまり、NonEmptyList[A]
が与えられた場合、そこには少なくとも1つのA
があることが分かります。
それがaverage
関数にどう影響するか見てみましょう。
import cats.data.NonEmptyList
def average(xs: NonEmptyList[Int]): Double = {
xs.reduceLeft(_+_) / xs.length.toDouble
}
これにより、average
関数はListが空であるかの検証がなくなり、Listの平均を計算するロジックに集中できます。これは、入力がシステムに入力されるプログラムの境界に入力の検証を移すという薦めとうまく結びついています。
NonEmptyListの構造
NonEmptyList
は次のように定義されています。
final case class NonEmptyList[+A](head: A, tail: List[A]) {
// Implementation elided
}
NonEmptyList
のhead
は空ではありません。一方、tail
は、List
に要素を0個以上含めることができます。
NonEmptyListの要素の定義
NonEmptyList
の重要な特徴は、全体性です。List
の場合、head
とtail
の両方が部分的であり、少なくとも1つの要素がある場合にのみ、明確に定義されます。
一方、NonEmptyList
は、空のNonEmptyList
を作成することは不可能であるため、head
やtail
などの操作が定義されていることを保証します。
NonEmptyListの生成
NonEmptyList
を作成するには、さまざまな方法があります。
one
の使用
引数が1つだけのNonEmptyList
を作成する場合は、NonEmptyList.one
を使用します。
NonEmptyList.one(42)
// res0: NonEmptyList[Int] = NonEmptyList(42, List())
of
の使用
NonEmptyList.of
の定義
def of[A](head: A, tail: A*): NonEmptyList[A]
少なくとも1つのA
とそれに続くtail
の可変引数を持つ引数Listを受け入れます。次のように呼び出してください。
NonEmptyList.of(1)
// res1: NonEmptyList[Int] = NonEmptyList(1, List())
NonEmptyList.of(1, 2)
// res2: NonEmptyList[Int] = NonEmptyList(1, List(2))
NonEmptyList.of(1, 2, 3, 4)
// res3: NonEmptyList[Int] = NonEmptyList(1, List(2, 3, 4))
接頭と最後の要素に通常のList[A]
をとるofInitLast
もあります。
NonEmptyList.ofInitLast(List(), 4)
// res4: NonEmptyList[Int] = NonEmptyList(4, List())
NonEmptyList.ofInitLast(List(1,2,3), 4)
// res5: NonEmptyList[Int] = NonEmptyList(1, List(2, 3, 4))
fromList
の使用
Option[NonEmptyList[A]]
を返すNonEmptyList.fromList
もあります。
NonEmptyList.fromList(List())
// res6: Option[NonEmptyList[Nothing]] = None
NonEmptyList.fromList(List(1,2,3))
// res7: Option[NonEmptyList[Int]] = Some(NonEmptyList(1, List(2, 3)))
最後に大事なのがこちら。List
の構文をimportすると.toNel
があります。
import cats.syntax.list._
List(1,2,3).toNel
// res8: Option[NonEmptyList[Int]] = Some(NonEmptyList(1, List(2, 3)))
fromFoldable
とfromReducible
の使用
NonEmptyList.fromFoldable
およびNonEmptyList.fromReducible
を使用できます。 2つの違いは、fromReducible
は空でないデータ構造でのみ使用できるため、戻り値の型のOption
を回避できることです。
ここでいくつかの例を示します。
import cats.implicits._
NonEmptyList.fromFoldable(List())
// res9: Option[NonEmptyList[Nothing]] = None
NonEmptyList.fromFoldable(List(1,2,3))
// res10: Option[NonEmptyList[Int]] = Some(NonEmptyList(1, List(2, 3)))
NonEmptyList.fromFoldable(Vector(42))
// res11: Option[NonEmptyList[Int]] = Some(NonEmptyList(42, List()))
NonEmptyList.fromFoldable(Vector(42))
// res12: Option[NonEmptyList[Int]] = Some(NonEmptyList(42, List()))
// Everything that has a Foldable instance!
NonEmptyList.fromFoldable(Either.left[String, Int]("Error"))
// res13: Option[NonEmptyList[Int]] = None
NonEmptyList.fromFoldable(Either.right[String, Int](42))
// res14: Option[NonEmptyList[Int]] = Some(NonEmptyList(42, List()))
// Avoid the Option for things with a `Reducible` instance
import cats.data.NonEmptyVector
NonEmptyList.fromReducible(NonEmptyVector.of(1, 2, 3))
// res15: NonEmptyList[Int] = NonEmptyList(1, List(2, 3))
以上で公式ドキュメントよりScala with CatsのNonEmptyList
について説明終わりです!
ありがとうございました!!🙇♂️