4 ms·
One thing that intrigues me about null is that conceptions of null seem to divide into two main families. In the first, a type nullable(T) is constructed from
by rogual 7y ago
One thing that intrigues me about null is that conceptions of null seem to divide into two main families.
In the first, a type nullable(T) is constructed from a type T by simply adjoining a "Null" value to it. So, a nullable integer type could take the values {Null, 0, 1, -1, 2, ...}
In the second, nullable(T) is more like a "box" that you have to get the value out of. This is what Haskell is doing with its Just & Maybe. Here the values of our nullable int would be {Nothing, Just 0, Just 1, ...}
That's actually a fairly big difference. For instance, in the first model of null, nullable(nullable(T)) = nullable(T), but in the second, those are distinct types.
Another big distinction is that in the first model, nullable(T) is a superset of T, where in the second, it's not.
I haven't seen anyone give a name to this distinction or talk about when the first is more useful vs. the second.
- jraph 7y agoThe second looks like an option type (and the first, a nullable type), are these the names you are after? https://en.wikipedia.org/wiki/Option_type https://en.wikipedia.org/wiki/Option_type
- kmill 7y agoThe first is a union type, and the second is a disjoint union type. The second is also known by the names of tagged unions, sum types, and discriminated unions. For the first to be useful, there needs to be some hidden disjoint union to implement it (how else would you know it's an int vs the adjoined null?) but the type equivalence rules would let unions collapse: union(T,Null,Null) = union(T,Null). Here's an illuminating example I saw once: consider a class with a member that represents a value that is known or unknown, and it might take some time to initialize the value. There are three states: uninitialized, initialized but unknown, and initialized and known. This can be modeled by nullable(nullable(T)). If the nullable collapses, you can't tell the difference between uninitialized and unknown anymore.
- BlackFly 7y agoI like having a semantic difference for async values (maybe not computed yet) and for optional values (maybe no value). Thus you get Future<Optional<T>>. It is of course helpful since the "Do this when a value happens, do that when a failure happens" algorithms can be encoded onto the future. Obviously you don't want people implementing busy waits. Edit: as to your overall point, I agree that this just cannot be modeled with null. The intention is just not capturable.
- chenglou 7y agoYeah, more practically speaking, this “flattening” behavior of nullable(nullable(foo)) = nullable(foo) makes type checking much harder. This is one of the things that’s subtle and hard to get right when you tackle a type system on top of an existing dynamic language. The general case also induces horrible type checking performance. Another example of flattening would be JS’ promises, where awaiting on nested promises “flattens” every level into one (in FP jargon, it’s map and bind combined into one function). With the right sugar, the non-flattening version, option type, is actually _much_ better for not just type checking, but also developer ergonomics. Consider if you want to distinguish a map missing a value, or having a value that happens to be null. Or an iterator that asks you to return null to signal termination, but you just happen to want to provide the value null.
- H8crilA 7y agoIn C++ that's the distinction between pointers and references (a reference cannot be null; creating a null reference is an undefined operation). Although it is much more clunky than in languages with algebraic data types. Who doesn't love the clarity of algebraic data types ...
- contravariant 7y ago>That's actually a fairly big difference. For instance, in the first model of null, nullable(nullable(T)) = nullable(T), but in the second, those are distinct types. The difference is basically whether you use the Monad structure explicitly or not. Whereas Nullable(Nullable(T)) might not equal Nullable(T) if you use the Monad definition, there is a natural transformation between the two, which the other method just uses implicitly to make the two equal. It's basically the difference between having a function which maps 'Just Nothing' to 'Nothing', or adding a rule to your programming language that 'Just Nothing == Nothing'. Now the advantage of using the Monad structure is that you've got an easy way of chaining multiple functions that depend on non-nullable arguments, but might not return a value. So if you've got f: X -> Maybe Y and g: Y -> Maybe Z, the monad structure makes it easy to chain the two into a function h: X -> Maybe Z (this operation is basically the definition of the Monad). With the implicit definition you need to put a check in between to ensure the first function doesn't return Null. This difficulty is basically the reason for the recent addition of ?. operator to C# which first checks for null before accessing a member of an object. Of course once you know it's a Monad you can also (ab)use any special notation a language might have for handling them. In python it's pretty tempting to just use: for y in f(x): for z in g(y): yield z to handle this particular situation. You just need to realise the equivalence between a function returning a nullable and a generator of at most 1 value. It doesn't really matter if you handle things explicitly or implicitly for this, but making the structure explicit helps take advantage of coincidences like this.