Monday, July 20, 2009

Fan vs. Scala: Nullability (including quizzes!)

Scala's great, but I personally prefer another new statically-typed language for the JVM, called Fan. I think some others might, too. This blog post is my next in a series of some reasons why I personally prefer Fan to Scala. Here was my next claimed advantage from my original post:

5. Fan types are not-nullable by default, with the concept built into the core of the language, instead of being "Option"al.

This particular subject is interesting. I've rarely seen ClassCastExceptions in practice, but I've seens plenty of NullPointerExceptions. Without good documentation (remember path of least resistance here), it's really hard to know whether a method expects to support null pointers or not. And in a method that has been around for months or years, it's hard to analyze whether all the callers (however far up the call chain) are passing in nulls or not. It's common to get null pointer exceptions in random places in your code all the time with enough developers and a large enough code base. And the solution to random breakage is ad hoc null checks and best-guess alternative behavior. That clutters up code and leaves expectations still unclear.

So, my answer is that nulls are usually evil. Apparently, I'm not alone in this opinion. Tony Hoare, for example, called null references his "billion dollar mistake" in ALGOL W.

My opinion of the right answer for Java is "don't use null if you can avoid it" and "document your methods as whether they support null" and "throw exceptions early if you get a null when you shouldn't". That takes a lot of manual effort in Java. And then there's the primitives not-nullable vs. objects nullable distinction, which is there mostly for convenience/performance. But it leaves the language more complicated than necessary.

So, both Fan and Scala seem to feel nulls are bad, too. They both discourage them. Fan avoids them by making every type not-nullable by default:
Str a := "hello"
Str? b := "world"
b = null // okay
a = b // NullErr run-time error
Str c := null // compile-time error

Lovely, lovely, in my opinion. The same type rules apply for method parameters. Yes, you should still document what your parameters mean, but your path of least resistance leaves you safe from nulls by default. And often, I think that's what coders mean, anyway. Usually, you get NullPointerExceptions because you just presume everything's not null, without even thinking about it. At least, that's how it often seems to me.

Also in Fan, the types 'Bool', 'Float' (64-bit), and 'Int' (64-bit) can be nullable or not. When not nullable, they are called "value types" because they are stored expanded (using 'boolean', 'double', and 'int' primitives in the JVM, for example). Fan also supports autoboxing along these lines. Happily, the '==' operator will compare values for these types (and is one of the overloadable operators for your own types, though I don't recommend overloading except for 'const' types in most cases). Anyway, this strategy also allows for faster math in many cases vs. purely reference-based languages. There are still some improvements needed relative to making the value types blend more transparently into the rest of the type system, but they are in the queue. The final goal is to make it blend almost seamlessly into the rest of the null-vs.-not-null type system.

Scala also recommends against null but in a very different way. They have very distinct reference vs. value types. All value types are predefined (I think) and mostly (but not entirely) correspond to the primitive types in Java. The reference types correspond to objects in Java. All reference types are nullable, but good style says don't use null. At least, when you don't need to interoperate with Java or whatnot. So, just keep in your head not to use null. Instead, you use 'Option' (usually, as there are other alternatives, too). My Fan examples above now look like this in Scala (if I'm not making any mistakes at the moment -- already fixed a few since my original post):
var a: String = "hello"
var b: Option[String] = Some("world")
b = None
a = b.get // NoSuchElementException run-time error
var c: String = None // compile-time error

I'm ignoring 'getOrElse' at the moment (just as I ignored the elvis '?:' operator in Fan above), by the way. I'm just focusing on the basic rules. My opinions on handling nulls when you have them are beyond my current scope.

So again, Fan has a (mostly) unified type system defaulting to not-null. Scala has a branched type system supporting value types on one side and and nullable references on the other, but don't use null. I think Fan's system is simpler, and I like that it pushes not-null into the strong position.

Well, I could be done here, but I'm not.

There's another word in Java that gives the same idea as the English word "null". That word is "void". If you know Java, you know what both keywords mean, but I think the relationship is interesting, and it brings up additional exploration for Fan vs. Scala, too. The value "null" is actually out there. It's a real thing that represents "no object". On the other hand, "void" really means "nothing". It doesn't exist. You can't assign it to anything. As a type, it is an empty set. I should also mention 'Void' in Java as the reflection-friendly type corresponding to the keyword 'void'.

Scala quiz time! Instead of describing what they mean, I'll give a list of similar concepts in Scala and see if you can match them with the correct meaning. If you read above, you'll get some of these. And maybe you can guess the others. First the Scala list of terms (all related to null and void in Java):

1. Null
2. null
3. Nothing
4. Nil
5. None
6. Unit
7. ()

And here are the definitions to match them against (in a mixed order):

A. The empty list.
B. The empty set type, corresponding to the meaning of 'void' in Java. The subtype of all value and reference types in Scala. The type parameter used for the empty list.
C. The single instance of type 'Unit'. I think it's purposely not supposed to mean anything.
D. The single instance of type 'Null'. Corresponds to 'null' in Java, but don't use it that way.
E. A subtype of all reference types in Scala whose only value is 'null'.
F. The instance of type 'Option' that means the option was not chose to be 'Some' value. Use with 'Option' types instead of 'null' for direct reference use. Also a kind of empty list.
G. A value type with only one (meaningless) member. Used in place of 'void', although it has a different meaning in my opinion.

Fan quiz time! I'm leaving out empty lists here, although you can have those in Fan just like in Java. They just don't have names or meanings so intertwined with 'null' and 'void', so just like Java, I think they are less relevant in Fan. Anyway, here are the terms I think should be considered:

1. null
2. Void
3. Void?

And here are the definitions to consider:

A. A type that corresponds quite closely to 'void' in Java. An empty set with no members, and only used for return types from functions.
B. A type with just the 'null' reference as a member. Somewhat related to the 'Unit' type in Scala, but I'm not sure it has any practical use. I think I'm glad it's there (just for consistency), and maybe it has some use I haven't figured out yet.
C. Corresponds to 'null' in Java. Assignable to vars of any nullable type.

Anyway, I hope this post was sufficiently readable. I think it's an important subject, and I personally like the solution that I think is simpler, more orthogonal, and less null-friendly. (And that particular personal preference is presumably easy to guess at this point.)

9 comments:

  1. Interesting comparison. I like Fan's approach a lot, particularly going non-null by default.

    Just to put in another viewpoint, I think there are also some benefits in having null/non-null done as a library in Scala. For example, you can use Option with the for-comprehension syntax -- I was recently pleasantly surprised by how much clearer some particular code ended up as a result of using this approach. There's a bunch of other stuff that you can do with Option as "just a library" that I suspect you'd need baked-in language syntax to do in Fan (such as the null-convenience operators).

    ReplyDelete
  2. Matt, thanks for the feedback. I appreciate additional viewpoints being added.

    ReplyDelete
  3. nil, (), Unit, null, Null, None, Nothing.

    hey, that was fun, and easy since there is no contextual overlap.

    ReplyDelete
  4. shineon, you got 4/7 correct. Maybe my wording wasn't ideal, though, so that could bias things. Hard to say. Any Scala experts out there able to put together better wording for me?

    ReplyDelete
  5. Nil, Nothing, (), null, Null, None, Unit

    ReplyDelete
  6. houndofulster, you got them all. Was my wording clear enough, in your opinion?

    ReplyDelete
  7. Hi Tom

    I'm no expert and in particular I don't really understand the Unit/() thing however I don't think it is true to say that Nothing is like void in java. As you say, Nothing is the bottom type of the scala type system, which is not true of void in java.

    Also I think this list of similar sounding concepts makes things seem more complicated than they really are in scala. None and Nil are just library classes. Null and null only really exist to allow interoperability with java, null is strongly discouraged in scala.

    ReplyDelete
  8. houndofulster, thanks for the feedback.

    ReplyDelete
  9. Scala's Nothing is the bottom type. It's the type of functions that don't return normally (they get stuck in a loop or throw an exception or whatever). Nothing is very useful for covariance (e.g. Nil is an instance of List[Nothing]) and for writing error methods that do some useful processing before throwing a constructed exception. The bottom type isn't expressible in Java.

    Java's void means that the function returns normally it just doesn't return anything useful. Scala's Unit is basically the same as Java's void but with fewer restrictions, e.g. List[Unit] is expressible in Scala but not Java. () is an object. It is the unique singleton instance of Unit and occasionally useful, e.g. when generic code requires a result but you just want side effects. Java's lack of a unit value and a decent semantics for void as a type is a thorn in the side of many attempts to write generic Java code. At least one of the closure proposals for Java adds a proper Void type with a value.

    null is a singleton object that is the only instance of Null. Null is a subtype of all reference types, hence all references may refer to the null object. Null sucks and is a wart in the Scala language from a type system point of view, but it's an important wart for seamless Java interop. Seamless Java interop is not one of Fan's design goals.

    Nullable types are strictly weaker than Option types because Nullable types can't be "stacked." If you have a Map from Ints to nullable Strings and you do a lookup on the number 3 then in Java you get a null. But you can't tell (without calling other methods) if that null was the value associated with 3 or if 3 just isn't a key in the map. Option types get rid of all that nonsense because you can say that you have a Map[Int,Option[String]]. If you get back None then 3 is not a key. If you get back Some[None] then 3 is mapped to None. Also, as Matt said, Option is a monad with very clean composability features. Note that None is not part of the language, it's part of the library. So it's not special.

    Note: it's not impossible for nullable types to be stackable - but at that point they aren't nullable types, they're option types with different syntax.

    Nil is just the end of a List. A List is either Nil (empty) or it is a cell with a datum called head plus a tail that is a List. It's no different from saying that a BinaryTree is either Empty or Node (with a datum and two child trees). Nil is not special, it is part of the library.

    ReplyDelete