4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NonEmptyList[Scala with Cats]の説明

Last updated at Posted at 2020-12-23

初めに

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
}

NonEmptyListheadは空ではありません。一方、tailは、Listに要素を0個以上含めることができます。

NonEmptyListの要素の定義

NonEmptyListの重要な特徴は、全体性です。Listの場合、headtailの両方が部分的であり、少なくとも1つの要素がある場合にのみ、明確に定義されます。

一方、NonEmptyListは、空のNonEmptyListを作成することは不可能であるため、headtailなどの操作が定義されていることを保証します。

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)))

fromFoldablefromReducibleの使用

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について説明終わりです!

ありがとうございました!!🙇‍♂️

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?