Skip to main content
Version: 2.x

Identity

Identity[A] describes a data type with an associative combining operation that also has an identity element.

Its signature is:

trait Associative[A] {
def combine(left: => A, right: => A): A
}

trait Identity[A] extends Associative[A] {
def identity: A
}

The identity element is a neutral value that does not have any structure itself and so combining it with any other value just leaves the structure of the other value unchanged. For instance, an empty Chunk is an identity element with respect to the ++ operator.

It is important to note here that while Identity describes a data type with an identity element, that identity element only has meaning in relation to the binary operator. Specifically, combining any value of type A with the identity element with the combine operator must return the value unchanged.

a <> identity === a
identity <> a === a

For example, zero is an identity element with respect to addition, but not with respect to multiplication. Similarly, true is an identity element with respect to logical conjunction, but false is an identity element with respect to logical disjunction.

Many associative combinining operations also have an identity element, but some do not.

For example the minimum or maximum of two values on which a total ordering is defined does not have an identity element. Neither does the first or last of two values or the concatenation of collections that cannot be empty.

If we import zio.prelude._ we can use the same <> operator as we did to combine two values for which an Associative instance was defined.

Since the identity value is a single value and not an operator we access it slightly different. Typically we will use the apply method on the Identity companion object.

import zio.prelude._
import zio.prelude.newtypes._

val zero: Int =
Identity[Sum[Int]].identity
// zero: Int = 0

The apply operator on the Identity companion object "looks up" the instance of the type class for the specified type, failing with a compilation error if the instance cannot be found. This operator is available on every functional abstraction in ZIO Prelude and is sometimes referred to as its "summoner".

Notice also here that we are using the Sum new type to specify that we are interested in the identity element with respect to addition rather than another operator such as multiplication. See the section on new types in the documentation for the Associative abstraction for a discussion of the use of new types to disambiguate type class instances.

The main value the identity element adds over just having the associative combining operation is the ability to handle cases where we do not have any value of the type at all.

With the Associative abstraction we know how to combine any two values of type A. So if we have one or more values of type A we can always combine them all together by repeatedly applying the combine operator.

However, we need at least one A value to get the process started. If we don't have any A value initially we have no way to create one with the Associative abstraction alone.

To see this, consider the example of computing the minimum value in a collection.

If the collection has exactly one element we can just return it. And if it has more than one element we can combine them using the min operator until we only have a single value.

But what do we do if the collection has no values? We can't do anything so we return an Option that is a Some with the minimum if there is at least one element and None otherwise.

import zio.prelude._
import zio.prelude.newtypes._

def min[A: Ord](as: List[A]): Option[A] =
Max.wrapAll(as) match {
case h :: t => Some(t.foldLeft(h)(_ <> _))
case Nil => None
}

Compare this to computing the sum of a collection. Now If the collection is empty we can just return the identity value.

def sum[A](as: List[A])(implicit identity: Identity[Sum[A]]): A =
Sum.wrapAll(as).foldLeft(identity.identity)(identity.combine(_, _))

The identity element handles the case where the collection is empty for us, allowing us to always return a summary value.

If we don't have an identity value we can only guarantee that we can return a summary value if the collection is known not to be empty, such as a NonEmptyChunk or a NonEmptyList. This is why these data types can be valuable.

If the collection could be empty and we only have a combine operator we may not be able to return a summary value and the best we can do is an Option.

One way to think of this is that the None case of the Option handles the possibility of failure. Another is that the None case of the Option provides the identity element for the combine operator, which we can express like this:

implicit def OptionIdentity[A: Associative]: Identity[Option[A]] =
new Identity[Option[A]] {
def combine(left: => Option[A], right: => Option[A]): Option[A] =
(left, right) match {
case (Some(a1), Some(a2)) => Some(a1 <> a2)
case (_, None) => left
case (None, _) => right
case (_, _) => None
}
def identity: Option[A] = None
}

This says that we can define an Identity instance for any Option[A] as long as there is an Associative instance defined for A. The Option provides the "free" structure of an identity element to go from just having an associative combine operator to having an identity element.

This can be useful to keep in mind if you are dealing with a collection that could be empty and need an Identity instance but only have an Associative instance. You can always map your collection type to an Option and then you will have an Identity instance.

Data types with an associative combine operator and an identity element are very common. With an Identity instance defined for them and the other tools in ZIO Prelude you are in a good position to handle working with even complex data types.