HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Similarities and differences between Unit and Bottom types?

Submitted by: @import:stackexchange-cs··
0
Viewed 0 times
similaritiesbottomdifferencesbetweentypesandunit

Problem

I came across this recent Reddit thread, Thoughts on Botton vs Unit Types, but I don't understand what the similarities and differences are in regards to when you are creating a programming language.

If you are creating a programming language, why do you need both a Bottom and a Unit type (as defined in the thread).

For the sake of this discussion, let's call the bottom type a type that has no members. NIL in Common Lisp, Nothing in Scala, never in TypeScript, Never in Swift. This type can be used to indicate the return type of a function that never returns (a function that only throws an exception, for example).

Let's call the unit type a type that has a single member. NULL and NIL in Common Lisp (the symbol NIL not to be confused with the type NIL), () and () in Haskel, Unit and () in Scala, Void and () in Swift.

What exactly are the differences between the two as well?

In my PL, I currently just have a void type, which is meant to mean "nothing" or "empty" or "null" I would think, but this throws into question what I am doing. It seems I need to add another type, but not sure how it fits in. Because then in Rust you have the None type, I don't see why I couldn't just make that the void type as well. That sort of stuff. What are the similarities and differences, and why do you need both?

Solution

Jörg W Mittag gives an excellent answer. But I think it does miss the heart of the question. The quote says "For the sake of this discussion, let's call the bottom type a type that has no members." So while Jorg is right that the "the defining feature of a bottom type is not that it has no members" and that "it is perfectly possible for a bottom type to have members", we should also consider that it is perfectly possible for a bottom type to have no members.

Why would a type with no members be useful and why can't we just use the unit type for the same thing?
Unit and None

So let's consider a language that has a type Unit with one member unit and a type None with no members. I'll assume that unit is not a member of "ordinary" types like Int. While we are at it, let's assume that the only members of Int are integers.
A case for Unit

If we declare a function

fun foo() : T is .... end


where T is a type, that usually means that if a call returns, it returns a value that is a member of type T. Or to put it another way

Any call to foo either returns a member of T or doesn't return

If T is Unit, that means that any call to foo will return (if it returns) a member of Unit, i.e. unit. Since the value returned is entirely predictable, there is no point writing say

val x : Unit = foo()


since the value of x is a forgone conclusion. We might as well just write

foo()


Unit is playing the role that void plays in C or Java. The only difference is that we are treating Unit as a type, whereas in C and Java void is not really a type.

So what about None, why not use that instead of Unit for our analog of void?

Consider

fun bar() : None is ... end


From our understanding of what this means, we see that

Any call to bar either returns a member of None or doesn't return

But since None has no members, this is equivalent to

No call to bar returns.

That doesn't sound like void. None is not a suitable analog for void, but Unit is.
Three cases for None

Is there any point to having None?

Here are three arguments for having a None type. But note that these arguments don't really rest on None having no members, but rather that it is a subtype of all other types in the language, i.e. that it is a bottom type.
None as a return type

A function that returns None sounds pretty useless. But it isn't completely useless. Suppose I have a function that always throws an exception, then I can write

fun unreachable() : None is
    throw new AssertionError("Unreacble code reached")
end


This can be useful for defensive programming. E.g.

fun partial(a : Int) : Int is
    if a > 0
        return 1 
    else if a < 0
        return -1
    else
        return unreachable()
    end
end


This should type-check since None is a subtype of Int.

What if we had used used Unit for the return type of unreachable? The subroutine above would not type check. Since Unit is not a subtype of Int). So we'd have to rewrite the else part as, for example

unreachable()
 return 42


which looks rather arbitrary.

Here is a more compelling example. Here we have a generic function

fun assertWithResult( b : Bool, v : T, m : String := "Assertion failed" ) : T is
    if b
        return v
    else
        return (throw new AssertionError( m ) )
    end
 end


For this to typecheck, we need for throw new AssertionError( m ) to have the bottom type. If we rewrite the code as

else
    throw new AssertionError( m )
end


The language implementation will (I'd hope) complain about the missing return. We can't rewrite it as

else
    throw new AssertionError( m )
    return ...
end


since there is nothing we can write in the place of .... null is not the answer because I'm supposing null is not a member of Int, for example. unit has the same problem. I'll consider the case where there is a null value that's in every type later.
None as the type of something that isn't there.

Suppose our language has lists. (These are lists of values, as in Haskell -- not lists of locations as in Python.)

A reasonable rule for list catenation is that the type of xs ++ ys is List[T|U] where the type of xs is List[T], the type of ys is List[U] and T|U is the smallest type that contains the union of the members of T and U.

What is the type of the empty list constant []. If it is List[Unit], then [1,2,3] ++ [] will have type List[Int|Unit], which is probably not what you want. But if the type of the empty list constant is List[None] we have List[Int|None] which is List[Int].
Intersection types

Let's introduce multiple inheritance into our language. Given two interfaces I and J we can write I&J for the combination of both. I.e. the members or I&J are exactly the members of I intersected with the members of J. Why not allow intersection types for any two types? Now what is Int&Unit? Again None comes

Code Snippets

fun foo() : T is .... end
val x : Unit = foo()
fun bar() : None is ... end
fun unreachable() : None is
    throw new AssertionError("Unreacble code reached")
end
fun partial(a : Int) : Int is
    if a > 0
        return 1 
    else if a < 0
        return -1
    else
        return unreachable()
    end
end

Context

StackExchange Computer Science Q#151088, answer score: 7

Revisions (0)

No revisions yet.