Ramblings about Void

Last modified on


Extra attributes on post: XmlAttr(name=original_link, value=String(value=https://gitter.im/scalaz/scalaz?at=5b246f7a6b24803e845c6a17))

This is basically a slightly cleaned up version of my answer to

Hey @E @A, Can you guys provide me simple example code which uses Void type in Scala? I think that we use Void when we know for sure that some branch is never going to execute kinda situation . But I am confused with the method absurd which seem to doing nothing, It takes Void and returns Void. confused Need your help!

1. Why do we need absurd?

Section missing id

Consider Void \/ A => A:

def coidr[A]: Void \/ A => A = {
  case -\/(x) => ...
  case \/-(a) => a
}

We need to insert something in the first case, but what? We have a value x : Void in scope, so we know for sure that that case can never actually succeed. But we still need to convince the compiler of that!

In some compilers, impossible cases can actually be marked as impossible, the compiler itself finds a value of type Void in the current context and then does dead code elimination.

But if you don't have such an option, you can use absurd[A](v: Void): A to satisfy any result type.

2. How do you use Void in practice?

Section missing id

Perhaps the most important use-case for Void is also the most "useless", or "computationally trivial": proving that things are false. If you prove that you can derive Void from some expression (or a logician would say assuming) a : A, you know that A has no inhabitants (no proofs). But that knowledge doesn't allow you to actually compute anything.

I will assume that the reader is somewhat familiar with propositions-as-types or Curry-Howard-Lambek correspondence. It is often misunderstood in the programming community IMHO.

Under CHL, (A => Void) => Void corresponds to (A→⊥)→⊥ or in other words ¬¬A. In classical logic it is the same as A, but in intuitionistic logic we can not remove the double negation.

You can try writing ((A => Void) => Void) => A in Scala, but I guarantee you that whatever tricks you use (mutability, exceptions), it is impossible to do for an arbitrary type A without running into soundness issues:

type Void <: Nothing
type \/[+A, +B] = Either[A, B]
type ¬¬[+A] = (A => Void) => Void

final case class Return[A](tag: AnyRef, a: A) extends Throwable {
  override def fillInStackTrace(): Throwable = this
}

def uncps[A](a: ¬¬[A]): A = {
  val Tag: AnyRef = new AnyRef()

  try a(a => throw Return[A](Tag, a)) catch {
    case Return(Tag, a) => a.asInstanceOf[A]
  }
}

// Law of excluded middle under double negation.
def either[A]: ¬¬[(A => Void) \/ A] =
  k => k(Left(a => k(Right(a))))

println(uncps(either[Int])) // Left(&lt;function>)

uncps(either[Int]) match {
  case Left(f) => f(1) // throws an exception
  case Right(x) => x
}

But it is still a very interesting type because we know that if someone managed to come up with (A => Void) => Void without cheating, then A must have at least one inhabitant.

Why? Let's assume that someone gave us a p: (A => Void) => Void without cheating (e.g. exceptions or non-termination). Let's cheat ourselves, and pass it na : a => ???. p(na) must return a value of type Void, so it either calls na, or our language itself is unsound and it is possible to derive a : Void without calling na.

In some cases we have escape hatches that allow us to forcibly construct an A, for example consider A =:= B:

def equal[A, B]: A =:= B =
  implicitly[A =:= A].asInstanceOf[A =:= B]

How do we know that this won't crash our code with a ClassCastException? What if someone gave us ((A =:= B) => Void) => Void? We would know that A =:= B is inhabited by at least one value, which means that A is indeed the same type as B and implicitly[A =:= A].asInstanceOf[A =:= B] is perfectly safe.

So in some cases for some types A it is perfectly legal to do ((A => Void) => Void) => A.

Logically, it is ¬¬A→A, proof by contradiction: you assume that A is false, prove absurdity from that, and by contradiction you prove A.

3. Void in category theory.

Section missing id

I'll assume that the reader is familiar with Scal or Hask as categories with types as objects and functions as morphisms.

Categories without any extra structures are very very boring. Luckily, Scal has lots of interesting things in it, e.g. it is both a cartesian and a co-cartesian category.

Now, one might say "But Alex, Scal is not really a category and eager languages do not have products".

And to that I say "Bite me". Yes, everything I said so far is true only under rather severe assumptions (totality, purity, parametricity, no null, no compiler weirdness, infinite stack, preferably TCO, language soundness, well-defined operational semantics, well-defined function equality, etc), but we make them all the time when doing functional programming anyway.

Anyway, let's unpack "cartesian" and "cocartesian". First of all they require initial and terminal objects.

Terminal objects have unique incoming morphisms from every other object.

Is Int a terminal object? Clearly not, because there are many functions Int => Int.

Is Unit a terminal object? Yes, because there is only one arrow A => Unit, that's _ => ().

Conversely, initial objects have unique outgoing morphisms to every other object.

Is Unit an initial object? No, because there are many functions of type Unit => Int, for example _ => 42 or _ => -1.

Now, it might not be exactly clear why, but Void is an initial object in Scal.

There is certainly a function from Void to every other A, absurd. But why is it unique? That's far less clear. It doesn't have any observable properties, we can not call an f : Void => A. There is no way to distinguish two f, g : Void => A.

You could say that precisely because they have no observable properties that are different, we should identify them.

Notice that when I say "Void is an initial object", I say an rather than the. That's because any type with no inhabitants would serve equally well as an initial object. In fact, in category theory you prove that they are all isomorphic.

4. Cartesian categories are monoidal

Section missing id

Okay, back to cartesian and cocartesian. Cartesian categories also have to be monoidal, with terminal object as a unit. Monoidal categories are those that have a special ⊗[_, _] such that (A ⊗ B) ⊗ C is isomorphic to A ⊗ (B ⊗ C), an identity object I such that A ⊗ I is isomorphic to A and I ⊗ A is isomorphic to A, and some other laws that are not important right now.

What if we take ⊗[A, B] = (A, B)? Clearly ((A, B), C) is isomorphic to (A, (B, C)). But what do we need to substitute for I in (A, I) so that it is isomorphic to A?

Unit, which is a terminal object! Which means Scal is a cartesian category.

Now, if you know a bit of category theory, you might say "(,) is a product, what about co-products"?

Turns out that if you take ⊗[A, B] = A \/ B, it also satisfies (A ⊗ B) ⊗ C = A ⊗ (B ⊗ C) (please take some time to write an function with type (A \/ B) \/ C => A \/ (B \/ C) to convince yourself). Now we need an I such that A \/ I is isomorphic to A.

We have absurd, so we can easily write A \/ Void => A, in fact we already have a mostly complete implementation at the beginning of this page. And A => A \/ Void is just -\/. So Void makes Scal into a cocartesian category.

5. But how does absurd work in Scala?

Section missing id

Finally, I hear you say "absurd seems to do nothing, it takes Void and returns Void". The reader must have looked at the implementation of absurd rather than its interface:

def absurd[A]: Void => A = v => v

Indeed, it takes a v: Void and returns it, because in Scala we have subtyping and a bottom type of the subtyping lattice Nothing. Nothing <: A for any A, and if A <: B, then (a : A) : B upcasts a to type B.

Nothing is actually the same type as Void, with the exact same semantics from the logical / categorical point of view.

But we work in Scala, and Scala is a massive PITA sometimes, especially when it comes to Nothing. it really dislikes Nothing in implicits and it is overly eager to report any use of Nothing as "dead code" and issue a warning if you have -Ywarn:unused (or whatever the flag).

So instead we hide Nothing behind an opaque newtype and export only the parts of its behavior we care about - absurd (and maybe Void === Nothing). This makes Scala happy and we can use Nothing without all of the bugs.