Skip to main content
Version: 2.0.x

NonEmptyForEach

NomEmptyForEach[F] describes a parameterized type F[A] that contains one or more values of type A.

Its signature is:

trait ForEach[F[+_]] {
def forEach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
}

trait NonEmptyForEach[F[+_]] extends ForEach[F] {
def forEach1[G[+_]: AssociativeBoth : Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]]
final def forEach[G[+_]: IdentityBoth: Covariant, A, B](fa: F[A])(f: A => G[B]): G[F[B]] =
forEach1(fa)(f)
}

trait Covariant[F[+_]] {
def map[A, B](f: A => B): F[A] => F[B]
}

trait AssociativeBoth[F[_]] {
def both[A, B](fa: => F[A], b: => F[B]): F[(A, B)]
}

trait IdentityBoth[F[_]] extends AssociativeBoth[F] {
def any: F[Any]
}

The NonEmptyForEach functional abstraction builds on the ForEach abstraction to describe a type that contains one or more A values rather than zero or more A values.

For example NonEmptyChunk has a NonEmptyForEach instance defined for it because it always contains at least one value. In contrast Chunk only has a ForEach instance defined for it because it could be empty.

NonEmptyForEach generalizes over collection types and types that always contain at least one value, such as NonEmptyChunk, NonEmptyList, and certain tree data structures.

The defining operator of the NonEmptyForEach abstraction is forEach1, which has the same signature as the forEach operator defined by the ForEach abstraction except that it doesn't require an identity element with respect to the combining operator for G values.

Recall that if instances of Covariant and AssociativeBoth exist for a data type we can define the map and zipWith operators and if an instance of IdentityBoth exists as well we can define the succeed operator.

def map[F[+_], A, B](fa: F[A])(f: A => B)(implicit covariant: Covariant[F]): F[B] =
covariant.map(f)(fa)

def succeed[F[+_], A](a: => A)(implicit covariant: Covariant[F], both: IdentityBoth[F]): F[A] =
covariant.map[Any, A](_ => a)(both.any)

def zipWith[F[+_], A, B, C](
fa: F[A],
fb: F[B]
)(f: (A, B) => C)(implicit covariant: Covariant[F], both: AssociativeBoth[F]): F[C] =
covariant.map(f.tupled)(both.both(fa, fb))

To see why we don't need an identity element for the combining operator when the collection contains at least one value let's compare the implementation of the ForEach instance for List with the implementation of the NonEmptyForEach instance for the NonEmptyList data type from ZIO Prelude.

import zio.prelude.NonEmptyList

implicit val ListForEach: ForEach[List] =
new ForEach[List] {
def forEach[G[+_]: IdentityBoth: Covariant, A, B](fa: List[A])(f: A => G[B]): G[List[B]] =
fa.foldRight(succeed[G, List[B]](List.empty)) { (a, gbs) =>
zipWith(f(a), gbs)(_ :: _)
}
}
// ListForEach: ForEach[List] = repl.MdocSession$MdocApp$$anon$1@2c58d616

implicit val NonEmptyListNonEmptyForEach: ForEach[NonEmptyList] =
new NonEmptyForEach[NonEmptyList] {
def forEach1[G[+_]: AssociativeBoth: Covariant, A, B](fa: NonEmptyList[A])(f: A => G[B]): G[NonEmptyList[B]] =
fa.reduceMapRight(a => map(f(a))(NonEmptyList.single))((a, gbs) => zipWith(f(a), gbs)(NonEmptyList.cons))
}
// NonEmptyListNonEmptyForEach: ForEach[NonEmptyList] = repl.MdocSession$MdocApp$$anon$2@3554e9b0

In our implementation of ForEach for List we needed to use the succeed operator to handle the case where the collection was empty, because in that case we had to be able to lift an empty collection into the context of G. The succeed operator requires an IdentityBoth instance because we have to be able to construct a neutral value of type G that we can then fill with the value we are lifting using the map operator.

In contrast, in our implementation of NonEmptyForEach we never need to call succeed because there is always at least one one element in the collection. So we can just apply the function f to each element in the collection and then use the zipWith operator to combine the results.

The fact that the collection can never be empty allows us to relax constraints on other operators as well.

For example, we can define an operator called reduceMapLeft that is a more powerful version of foldLeft that does not require an initial value.

import zio.prelude._

def reduceMapLeft[F[+_]: ForEach, A, S](as: F[A])(map: A => S)(reduce: (S, S) => S): S =
as.foldLeft[Option[S]](None) {
case (Some(s), a) => Some(reduce(s, map(a)))
case (None, a) => Some(map(a))
}.get

We know it is safe to call get here because the collection is guaranteed to have at least one element.

The reduceMapLeft operator allows us to define additional operators for reducing a collection to a summary value that would not be safe to call on a collection that might be empty.

In particular, we can define a more powerful version of the foldMap operator defined on the ForEach abstraction called reduceMap.

def reduceMap[F[+_]: ForEach, A, B: Associative](as: F[A])(f: A => B): B =
reduceMapLeft(as)(f)(_ <> _)

Since we know the collection contains at least one element we do not need an identity value with respect to the associative combine operator, just like we did not need an any value with respect to the both operator in forEach1.

With this we can easily do something like calculate the sum, product, min, and max of a collection in a single pass.

import zio.prelude.newtypes._

def stats[F[+_]: ForEach, A](as: F[A])(
implicit sum: Associative[Sum[A]],
prod: Associative[Prod[A]],
min: Associative[Min[A]],
max: Associative[Max[A]]
): (A, A, A, A) =
reduceMap(as)(a => (Sum[A](a), Prod[A](a), Min[A](a), Max[A](a)))

This is a very nice way to describe reducing a collection to a summary value and gives us additional flexibility to use ways of combining that are associative but do not have an identity element relative to the foldMap operator.

As we can see, the NonEmptyForEach functional abstraction builds on the ForEach functional abstraction to describe parameterized types that contain one or more values of the type they are parameterized on. This lets us define a wide variety of operators, even more than the ones we could define for the ForEach abstraction.

So if you are defining your own data type like a collection and it will never be empty you should definitely define a NonEmptyForEach instance for it so you can take advantage of all the nice operators that are defined in terms of it.