11
$\begingroup$

While creating a client for a web API in C#, I ran into a problem regarding null as a value where it would represent two different things:

  • nothing, e.g. a foo may or may not have a bar
  • unknown: by default the API response only includes a subset of properties, you have to indicate which additional properties you want. So unknown means that the property was not requested from the API.

After some searching I found out about the Maybe (or Option) type, how it's used in functional languages and how it "solves" null dereferencing problems by forcing the user to think about the possible absence of a value. However, all of the resources I encountered talked about replacing null with Maybe. I did find some mentions of three-valued logic, but I don't fully understand it and most of the times its mentioning was in the context of "it's a bad thing".

I'm now wondering if it makes sense to have both the concept of null and Maybe, to represent unknown and nothing respectively. Is this the three-valued logic I read about, or does it have another name? Or is the intended way to nest a Maybe in a Maybe?

$\endgroup$
11
  • 9
    $\begingroup$ It does not make sanse to have null. It is a thoroughly broken idea. $\endgroup$ Commented Dec 30, 2016 at 14:16
  • 7
    $\begingroup$ Do not make a maybe-maybe-foo that has different semantics than a maybe-foo. Maybe is a monad, and one of the monad laws is that M M x and M x should have the same semantics. $\endgroup$ Commented Dec 30, 2016 at 15:19
  • 2
    $\begingroup$ You might consider looking at the design of early versions of Visual Basic, which had Nothing (reference to no object), Null (database null semantics), Empty (a variable not initialized), and Missing (an optional parameter was not passed). This design was complicated and in many ways inconsistent, but there is a reason why those concepts were not conflated with each other. $\endgroup$ Commented Dec 30, 2016 at 15:24
  • 3
    $\begingroup$ Yes, I wouldn't use Maybe Maybe either. But, in fact, just as Maybe a is the same as $a + 1$, semantically, Maybe Maybe a is the same as $a+1+1$, and isomorphic to UserInput a type from @Andej answer. You could define your own monad instance for this type too and therefore use different monad combinators. $\endgroup$
    – Euge
    Commented Dec 30, 2016 at 16:04
  • 12
    $\begingroup$ @EricLippert: it is false that "M (M x) and M x should have the same semantics". Take M = List for instance: lists of lists are not the same thing as lists. When M is a monad, there is a transformation (namely the monad multiplication) from M (M x) to M x which explains the relationship between them, but they do not have "the same semantics". $\endgroup$ Commented Dec 30, 2016 at 20:25

4 Answers 4

14
$\begingroup$

The null value as a default present everywhere is just a really broken idea, so forget about that.

You should always have exactly the concept that best describes your actual data. If you need a type which indicates "unkown", "nothing", and "value", you should have precisely that. But if it does not fit your actual needs then you should not do it. It is a good idea to see what other people use and what they have proposed, but you do not have to follow them blindly.

People who design standard libraries try to guess common usage patterns, and they usually do it well, but if there's a specific thing you need, you should define it yourself. For instance, in Haskell, you could define:

data UnknownNothing a =
     Unknown
   | Nothing
   | Value a

It may even be the case that you should be using something more descriptive that is easier to remember:

data UserInput a =
     Unknown
   | NotGiven
   | Answer a

You could define 10 such types to be used for different scenarios. The drawback is that you won't have available pre-existing library functions (such as those of Maybe), but this usually turns out to be a detail. It's not that hard to add your own functionality.

$\endgroup$
5
  • $\begingroup$ Makes perfect sense when you see it spelled out like that. I'm mostly interested in ideas, existing library support is just a bonus :) $\endgroup$
    – Stijn
    Commented Dec 30, 2016 at 14:35
  • $\begingroup$ If only the barrier to creating such types were lower in many languages. :/ $\endgroup$
    – Raphael
    Commented Jan 4, 2017 at 8:59
  • $\begingroup$ If only people stopped using languages from 1960's (or their reincarnations from 1980's). $\endgroup$ Commented Jan 4, 2017 at 9:05
  • $\begingroup$ @AndrejBauer: what? but then theoreticians wouldn't have anything to complain about! $\endgroup$
    – Yttrill
    Commented Jan 5, 2017 at 20:27
  • $\begingroup$ We don't complain, we're aloft. $\endgroup$ Commented Jan 8, 2017 at 11:53
4
$\begingroup$

As far as I know, the value null in C# is a possible value for some variables, depending on its type (am I right?). For example, instances of some class. For the rest of the types (like int, bool, etc) you can add this exception value by declaring variables with int? or bool? instead (this is exactly what the Maybe constructor does, as I will describe next).

The Maybe type constructor from functional programming adds this new exception value for a given datatype. So if Int is the type of integers or Game is the type of the state of a game, then Maybe Int has all the integers plus a null (sometimes called nothing) value. The same for Maybe Game. Here, there are no types that come with the null value. You add it when you need it.

IMO, this last approach is the better one for any programming language.

$\endgroup$
5
  • $\begingroup$ Yes, your understanding of C# is correct. So can I deduce from your answer that you suggest nesting Maybes and dropping null entirely? $\endgroup$
    – Stijn
    Commented Dec 30, 2016 at 14:38
  • 2
    $\begingroup$ My opinion is about how a language should be. I don't really know the best practices of C# programming. In your case, the best option would be defining a datatype as @Andrej Bauer described, but I think you can't do that in C#. $\endgroup$
    – Euge
    Commented Dec 30, 2016 at 14:45
  • $\begingroup$ @Euge: Algebraic Sum Types can be (roughly) approximated with classical OO-style subtyping and subtype polymorphic message dispatch. That's how it's done in Scala, for example, with an additional twist to allow for closed types. The type becomes an abstract superclass, the type constructors become concrete subclasses, pattern matching over the constructors can be approximated either by moving the cases into overloaded methods in the concrete subclasses or isinstance tests. Scala also has "proper" pattern matching as well, and C♯ actually just recently gained simple patterns, too. $\endgroup$ Commented Dec 30, 2016 at 15:00
  • $\begingroup$ The "additional twist" I mentioned for Scala is that in addition to the "standard" modifiers "virtual" (for a class that can be inherited from) and final (for a class that cannot be inherited from), it also has sealed, for a class that can be extended only within the same compilation unit. This means that the compiler can statically know all possible subclasses (provided all of them are either themselves sealed or final) and thus do exhaustiveness checks for pattern matches, which is not possible statically for a language with runtime code loading and unrestricted inheritance. $\endgroup$ Commented Dec 30, 2016 at 15:02
  • $\begingroup$ Of course you can design types with those semantics in C#. Note that C# does not allow the built-in maybe type (called Nullable in C#) to be nested. int?? is illegal in C#. $\endgroup$ Commented Dec 30, 2016 at 15:27
3
$\begingroup$

If you can define type unions, like X or Y, and if you have a type named Null which represents only the null value (and nothing else), then for any type T, T or Null is actually not so different from Maybe T. The only problem with null is when the language treats a type T as T or Null implicitly, making null a valid value for every type (except primitive types in languages that have them). This is what happens for example in Java where a function which takes a String also accepts null.

If you treat types as set of values and or as union of sets, then (T or Null) or Null represents the set of values T ∪ {null} ∪ {null}, which is the same as T or Null. There are compilers which perform this kind of analyses (SBCL). As said in comments, you cannot easily distinguish between Maybe T and Maybe Maybe T when you view types as domains (on the other hand, that view is quite useful for dynamically typed programs). But you could also keep types as algebraic expressions, where (T or Null) or Null represents multiple levels of Maybes.

$\endgroup$
1
  • 2
    $\begingroup$ Actually, it's important that Maybe (Maybe X) is not the same thing as Maybe X. Nothing is not the same thing as Just Nothing. Conflating Maybe (Maybe X) with Maybe X makes it impossible to treat Maybe _ polymorphically. $\endgroup$ Commented Dec 30, 2016 at 22:44
1
$\begingroup$

You need to find out what you wish to express, and then find a way how to express it.

First, you have values in your problem domain (like numbers, strings, customer records, booleans, etc). Second, you have some additional, generic values on top of your problem domain values: For example "nothing" (the known absence of a value), "unknown" (no knowledge about presence or absence of a value), not mentioned was "something" (there is definitely a value, but we don't know which one). Maybe you can think of other situations.

If you wanted to be able to represent all the values plus these three additional values, you would create an enum with cases "nothing", "unknown", "something", and "value" and go from there.

In some languages some cases are simpler. For example, in Swift you have for every type T the type "optional T" which has as its possible values nil plus all the values of T. This lets you handle many situations easily, and is perfect if you have "nothing" and "value". You could use it if you have "unknown" and "value". You could not use it if you need to handle "nothing", "unknown" and "value". Other languages may use "null" or "maybe", but that's just another word.

In JSON, you have dictionaries with key/value pairs. Here, a key may just be missing from a dictionary, or it may have a value of null. So by carefully describing how things are stored, you can represent generic values plus two additional values.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.