Skip to main content
Version: 2.x

Inverse

Inverse[A] describes a type that has a combine operator and also has an inverse operator that is the inverse of the combine operator.

Its signature is:

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

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

trait Inverse[A] extends Identity[A] {
def inverse(left: => A, right: => A): A
}

For example, subtraction is an inverse operator with addition being the combine operator and 0 being the identity value.

While the combine operator adds structure, the inverse operator takes it away, undoing the structure that the combine operator added. As a result, applying the inverse operator to a value and itself takes away all the structure, returning the identity element.

inverse(a, a) === identity

It is important to note that inverse is a binary operator rather than a unary operator.

When we think of an inverse with respect to integer addition it is tempting to think of the inverse as the unary operator of negation. This does indeed have the attractive property that the sum of any value and its inverse is zero.

However, this definition turns out to be quite limiting because it does not allow us to describe an inverse for types that do not contain negative values.

To see this, consider the case of natural numbers, represented by the Natural new type in ZIO Prelude. By definition there are no negative natural numbers and thus we cannot define a unary inverse operator for Natural.

However, natural numbers have a well defined notion of subtraction so it feels like we are missing something here. The solution is to view inverse as a binary operator.

We can then define an Inverse instance for Natural like this:

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

implicit val NaturalInverse: Inverse[Natural] =
new Inverse[Natural] {
def combine(left: => Natural, right: => Natural): Natural =
Natural.plus(left, right)
val identity: Natural =
Natural.zero
def inverse(left: => Natural, right: => Natural): Natural =
Natural.minus(left, right)
}
// NaturalInverse: Inverse[Natural] = repl.MdocSession$MdocApp0$$anon$1@1c5ac64a

This also lets us define Inverse instances for other data types that we would not otherwise be able to. For example, what is the inverse of Set(1, 2, 3)?

Logically it is the set of all integers other than 1, 2, and 3 but we have no way of efficiently representing that because a Set is a collection of concrete values. So we would not be able to define an Inverse instance for Set this way.

On the other hand, by defining Inverse as a binary operator we can quite easily do so.

implicit def SetInverse[A]: Inverse[Set[A]] =
new Inverse[Set[A]] {
def combine(left: => Set[A], right: => Set[A]): Set[A] =
left | right
val identity: Set[A] =
Set.empty
def inverse(left: => Set[A], right: => Set[A]): Set[A] =
left &~ right
}

The inverse operator is set difference, with the combine operator being set union and the empty set being the identity value.

The inverse abstraction tends to be used less frequently than the other abstractions for combining values. In writing programs, we typically want to build up more complex values from simpler ones, even if it is just tearing down one data type to build another.

The Inverse abstraction is most useful when we want to generalize over the notion of "subtraction" for different types. For example, if we are working with maps we might be interested in defining an operation that lets us return the difference between one map and another.

def diff[A, B: Equal : Inverse](left: Map[A, B], right: Map[A, B]): Map[A, B] =
right.foldLeft(left) { case (map, (a, b)) =>
val b1 = Inverse[B].inverse(map.getOrElse(a, Identity[B].identity), b)
if (b1 === Identity[B].identity) map - a else map + (a -> b1)
}

We look up the value for every key in the right map in the left map, returning the identity value if it does not exist. We then compute the difference using the inverse operator.

If the difference is equal to the identity value we remove the key from the map. Otherwise we add it.

Let's see it in action.

val peopleWhoOweMe: Map[String, Sum[Int]] =
Map("Alice" -> Sum(1000), "Bob" -> Sum(1000))
// peopleWhoOweMe: Map[String, Sum[Int]] = Map("Alice" -> 1000, "Bob" -> 1000)

val peopleIOwe: Map[String, Sum[Int]] =
Map("Alice" -> Sum(1000), "Carol" -> Sum(1000))
// peopleIOwe: Map[String, Sum[Int]] = Map("Alice" -> 1000, "Carol" -> 1000)

val myNetFinancialPosition: Map[String, Int] =
diff(peopleWhoOweMe, peopleIOwe)
// myNetFinancialPosition: Map[String, Int] = Map(
// "Bob" -> 1000,
// "Carol" -> -1000
// )

val friends: Map[String, Set[String]] =
Map("Alice" -> Set("Bob", "Carol"), "Bob" -> Set("Alice", "Carol"))
// friends: Map[String, Set[String]] = Map(
// "Alice" -> Set("Bob", "Carol"),
// "Bob" -> Set("Alice", "Carol")
// )

val unfriends: Map[String, Set[String]] =
Map("Alice" -> Set("Bob", "Carol"), "Bob" -> Set("Alice"))
// unfriends: Map[String, Set[String]] = Map(
// "Alice" -> Set("Bob", "Carol"),
// "Bob" -> Set("Alice")
// )

val updatedFriends: Map[String, Set[String]] =
diff(friends, unfriends)
// updatedFriends: Map[String, Set[String]] = Map("Bob" -> Set("Carol"))

In the first example the two maps represent assets and liabilities. The difference between them is our net financial position with each of our counterparties.

In the second example the first map represents the friends of users of a social networking site and the second represents friend removals for each user that need to be processed. The difference between them is the updated friends for each user we should return.

Often you won't need to use the Inverse abstraction directly. For example, functionality similar to the above is provided by the ZSet data type in ZIO Prelude.

However, when you do need to work some notion of subtraction Inverse is here and will let you describe that shared structure in a composable way.