NonEmptyList
API Documentation: NonEmptyList
Motivation
We start with two examples of NonEmptyList
s
Usage in Validated
and Ior
If you have had the opportunity of taking a look to
Validated or Ior, you'll find that a
common case is to use NonEmptyList
with one of these data
structures.
Why? Because it fits nicely in the error reporting cases. As stated by
its name, NonEmptyList
is a specialized data type that has at
least one element. Otherwise it behaves like a normal List
. For
sum types like Validated
(and Ior
), it does not make sense to have
a Invalid
with no errors: no errors means it is a Valid
! By using
NonEmptyList
, we explicitly say in the type that:
If it is a Invalid
, then there is at least one error.
This is much more precise and we don't have to wonder whether the list of errors might be empty when reporting them later on.
Avoiding Option
by demanding more specific arguments
As functional programmers, we naturally shy away from partial
functions that can throw exceptions like the famous head
method on,
e.g., List
.
Let's take as an example a method that calculates the average
:
def average(xs: List[Int]): Double = {
xs.sum / xs.length.toDouble
}
Clearly, this is not a valid definition for empty lists, because
division by zero will throw an exception. To fix this, one way is to
return an Option
instead of a Double
right away:
def average(xs: List[Int]): Option[Double] = if (xs.isEmpty) {
None
} else {
Some(xs.sum / xs.length.toDouble)
}
That works and is safe, but this only masks the problem of accepting
invalid input. By using Option
, we extend the average
function
with the logic to handle empty lists. Additionally, all callers have
to handle the Option
cases, maybe over and over again. While better
than failing with an exception, this is far from perfect.
Instead what we would like to express is that average
does not make
sense at all for an empty list. Luckily, cats defines the
NonEmptyList
. As the name says, this represents a list that
cannot, by construction, be empty. So given a NonEmptyList[A]
you
can be sure that there is at least one A
in there.
Let's see how that impacts your average
method:
import cats.data.NonEmptyList
def average(xs: NonEmptyList[Int]): Double = {
xs.reduceLeft(_+_) / xs.length.toDouble
}
With that, average
is free of any "domain invariant validation" and
instead can focus on the actual logic of computing the average of the
list. This ties in nicely with the recommendation of shifting your
validation to the very borders of your program, where the input enters
your system.
Structure of a NonEmptyList
NonEmptyList
is defined as follows:
final case class NonEmptyList[+A](head: A, tail: List[A]) {
// Implementation elided
}
The head
of the NonEmptyList
will be non-empty. Meanwhile, the
tail
can have zero or more elements contained in a List
.
Defined for all its elements
An important trait of NonEmptyList
is the totality. For List
specifically,
both head
and tail
are partial: they are only well-defined if it
has at least one element.
NonEmptyList
on the other hand, guarantees you that operations like
head
and tail
are defined, because constructing an empty
NonEmptyList
is simply not possible!
Constructing a NonEmptyList
To construct a NonEmptyList
you have different possibilities.
Using one
If you want to construct a collection with only one argument, use
NonEmptyList.one
:
NonEmptyList.one(42)
// res0: NonEmptyList[Int] = NonEmptyList(head = 42, tail = List())
Using of
The NonEmptyList.of
method on the companion of NonEmptyList
as the signature:
def of[A](head: A, tail: A*): NonEmptyList[A]
It accepts an argument list with at least one A
followed by a
varargs argument for the tail
. Call it like this:
NonEmptyList.of(1)
// res1: NonEmptyList[Int] = NonEmptyList(head = 1, tail = List())
NonEmptyList.of(1, 2)
// res2: NonEmptyList[Int] = NonEmptyList(head = 1, tail = List(2))
NonEmptyList.of(1, 2, 3, 4)
// res3: NonEmptyList[Int] = NonEmptyList(head = 1, tail = List(2, 3, 4))
There also is ofInitLast
which takes a normal List[A]
for the
prefix and a last element:
NonEmptyList.ofInitLast(List(), 4)
// res4: NonEmptyList[Int] = NonEmptyList(head = 4, tail = List())
NonEmptyList.ofInitLast(List(1,2,3), 4)
// res5: NonEmptyList[Int] = NonEmptyList(head = 1, tail = List(2, 3, 4))
Using fromList
There is also NonEmptyList.fromList
which returns an
Option[NonEmptyList[A]]
:
NonEmptyList.fromList(List())
// res6: Option[NonEmptyList[Nothing]] = None
NonEmptyList.fromList(List(1,2,3))
// res7: Option[NonEmptyList[Int]] = Some(
// value = NonEmptyList(head = 1, tail = List(2, 3))
// )
Last but not least, there is .toNel
if you import the syntax for
list
:
import cats.syntax.list._
List(1,2,3).toNel
// res8: Option[NonEmptyList[Int]] = Some(
// value = NonEmptyList(head = 1, tail = List(2, 3))
// )
Using fromFoldable
and fromReducible
Even more general, you can use NonEmptyList.fromFoldable
and
NonEmptyList.fromReducible
. The difference between the two is that
fromReducible
can avoid the Option
in the return type, because it
is only available for non-empty datastructures.
Here are some examples:
import cats.syntax.all._
NonEmptyList.fromFoldable(List())
// res9: Option[NonEmptyList[Nothing]] = None
NonEmptyList.fromFoldable(List(1,2,3))
// res10: Option[NonEmptyList[Int]] = Some(
// value = NonEmptyList(head = 1, tail = List(2, 3))
// )
NonEmptyList.fromFoldable(Vector(42))
// res11: Option[NonEmptyList[Int]] = Some(
// value = NonEmptyList(head = 42, tail = List())
// )
NonEmptyList.fromFoldable(Vector(42))
// res12: Option[NonEmptyList[Int]] = Some(
// value = NonEmptyList(head = 42, tail = 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(
// value = NonEmptyList(head = 42, tail = 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(head = 1, tail = List(2, 3))