10

In Idris, the Maybe type is defined as followed:

data Maybe a = Just a | Nothing  

It's defined similarly in Haskell:

 data Maybe a = Just a | Nothing
     deriving (Eq, Ord)

Here's the ML version:

datatype 'a option = NONE | SOME of 'a

What are the benefits of using Just and Some?
Why not define the type without them?

example:

data Maybe a = a | Nothing
Trevor Hickey
  • 36,288
  • 32
  • 162
  • 271
  • 12
    `data Maybe a = a | Nothing` is not valid Haskell—it won’t compile. That definition would effectively imply `Maybe a` is a *union* of any value or `Nothing`, but that’s pretty useless, since `Nothing` is also “any value”. (Haskell’s type system doesn’t support untagged unions like this at all, since that would require some notion of “subtyping”, which Haskell does not have.) – Alexis King Sep 19 '16 at 22:18
  • No, `Maybe a` would be a union of `a` or `Nothing`, and `Nothing` would normally not be a value of type `a`, so a type like `Maybe Int` wouldn't be useless. Correct that Haskell doesn't support this though. – Reid Barton Sep 20 '16 at 15:11

6 Answers6

26

What would then be the difference between

Maybe a

and

Maybe (Maybe a)

?

There's supposed to be a difference between Nothing and Just Nothing.

pigworker
  • 43,025
  • 18
  • 121
  • 214
  • 1
    For some reason, I am amused whenever I find that I have to type `pure Nothing` :). – Steven Shaw Sep 20 '16 at 12:25
  • 1
    But why is there supposed to be a difference? – Reid Barton Sep 20 '16 at 15:11
  • 2
    @Reid Because applying `Maybe` to a type creates a new type with one additional value. This means that applying it twice should create a new type with two additional values. – Tanner Swett Sep 20 '16 at 16:13
  • This is not a joke, in my experience, `Just Nothing` occurs not too often, but from time to time it does. – Ingo Sep 20 '16 at 18:34
  • 2
    Of course the semantics of Haskell (and Idris etc.) say that `Nothing` and `Just Nothing` are different, but this is just pushing back the question. – Reid Barton Sep 20 '16 at 18:49
  • 2
    And it's entirely reasonable to consider failure sooner the same as failure later, if you consider failure a computational effect (and have no other observables that happen before the failure). – pigworker Sep 20 '16 at 19:00
  • 3
    This comes up in the book [Land of Lisp](https://books.google.is/books?id=9apQfCRhvm0C&lpg=PA62&dq=%22land%20of%20lisp%22%20%22nil%22%20%20tear&hl=is&pg=PA61#v=onepage&q&f=false): `(find-if #'null '(2 4 nil 6))` correctly returns the element satisfying the `null` predicate (`nil`) but this is the same result if no elements had satisfied the predicate! Haskell is much clearer: `find (== Nothing) [Just 2, Just 4, Nothing, Just 6]` succeeds with `Just Nothing` (because it found the `Nothing` in the list) but `find (== Nothing) [Just 2, Just 4, Just 6]` fails with `Nothing`. – Iceland_jack Sep 24 '16 at 13:29
  • @Ingo it happens more often than you think, when using abstractions that you never thought would use `Maybe` inside –  Oct 24 '16 at 11:52
11

The key problem of allowing any value to be null (the "billion-dollar mistake") is that interfaces receiving a value of a type T have no way to declare whether or not they can handle a null, and interfaces that provide one have no way to declare whether or not they might produce null. This means that all of the operations that are usable on T essentially "might not work" when you pass them a T, which is a pretty gaping hole in all of the guarantees supposedly provided by compile-time type-checks.

The Maybe/Optional solution to this is to say that the type T does not contain a null value (in languages that had this from the beginning, that's literal; in languages adopting an Optional type later without removing support for null, then that's only a convention that requires discipline). So now all of the operations whose type says they accept a T should work when I pass them a T, regardless of where I got the T (if you haven't managed to design to "make illegal states unrepresentable" then there will of course be other reasons why an object can be in an invalid state and cause failure, but at least when you pass a T there'll actually be something there).

Sometimes we do need a value that can be "either a T or nothing". It's such a common case that pervasive null seemed like a good idea at the time, after all. Enter the Maybe T type. But to avoid falling back into exactly the same old trap, where I get a possibly-null T value and pass it to something that can't handle null, we need that none of the operations on T can be used on a Maybe T directly. Getting a type error from trying to do that is the entire point of the exercise. So my T values can't be directly members of Maybe T; I need to wrap them up inside a Maybe T, so that if I have a Maybe T I'm forced to write code that handles both cases (and only in the case for actually having a T can I call operations that work on T).

Whether this makes a word like Just or Some appear in the source code, and whether or not this is actually implemented with additional boxing/indirection in memory (some languages do represent a Maybe T as a nullable pointer to T internally), all of that is irrelevant. But the Just a case must be different from simply having an a value.

Ben
  • 68,572
  • 20
  • 126
  • 174
  • This argument falls apart somewhere near the end, since languages with subtyping can have real optional (or more generally, union) types. You argue that `T` and `Maybe T` must be different, but it doesn't follow that a value of type `T` can't also have type `Maybe T`. – Reid Barton Sep 20 '16 at 15:17
9

I am not sure it is correct to speak of "benefits" in this context. What you have here is just a consequence of the way types are implemented in Haskell and ML - basically, Hindley-Milner algebraic type system. This type system essentially assumes that every value belongs to a single type (putting aside Haskell's numeric tower and bottom, which are outside of this discussion.) In other words, there is no subtyping, and that's for a reason - otherwise the type inference would be undecidable.

When you define type Maybe a what you want is to adjoin a single additional value to the type denoted by a. But you can't do it directly - if you could then every value of a would belong to two different types - the original a and Maybe a. Instead, what is done is a is embedded in a new type - you have a canonical injection a -> Just a. In other words, Maybe a is isomorphic to a union of a and Nothing which you can't represent directly in HM type system.

So I don't think that arguments along the lines that such a distinction is beneficial are valid - you can't have a system without it which is still ML or Haskell or any familiar HM-based system.

Yuri Steinschreiber
  • 2,648
  • 2
  • 12
  • 19
1

The problem is that if Maybe was defined the way you propose, i.e. data Maybe a = a | Nothing there would be no way to differentiate a values from Maybe a values (and Maybe (Maybe a) for that matter).

So you may ask, why do we need to have such a distinction? What are the benefits? To give you a concrete example, suppose that we have a SQL table with a integer NOT NULL column. We would represent that with an Int in haskell. Now if we later on changed the database schema to make the column optional by dropping the NOT NULL constraint, we would have to change the haskell representation of the column to Maybe Int. The clear distinction between Int and Maybe Int would make it very easy to refactor our haskell code to account for the new schema. The compiler would complain for things such as extracting a value from the db and treating it as an Int (it might not be an integer, it might be NULL).

redneb
  • 21,794
  • 6
  • 42
  • 54
0

The benefit of the constructor (Just or Some) is that it provides a way to distinguish between the branches of the data type. That is, it avoids ambiguity.

For example if we were relying on type inference, then the type of x in the following seems fairly straightforward — String.

x = "Hello"

However, if we allowed your definition of Maybe, how would we know whether x was String, a Maybe String or a Maybe (Maybe String) etc.

Also consider a data type with more than two cases:

data Tree a
  = Empty
  | Node (Tree a) (Tree a)
  | Leaf a

If we simply removed the constructors (other than Empty), following your suggestion for Maybe, we'd end up with:

data Tree a
  = Empty
  | (Tree a) (Tree a)
  | a

I hope you can see that the ambiguity gets even worse.

Steven Shaw
  • 6,063
  • 3
  • 33
  • 44
0

Consider this somewhat equivalent of Maybe in C++/Java'ish psuedocode...

template<class T>
abstract class Maybe<T> { ... }

template<class T>
class Just<T> : Maybe<T> {
    // constructor
    Just<T> (T val) { ... }

    ...
}

template<class T>
class Nothing<T> : Maybe<T> {
    // constructor
    Nothing () { ... }

    ...
}

That is not specific to Maybe, it can be applied to any ADT. Now what exactly will

data Maybe a = a | Nothing

model into ? (assuming that its legal syntax).

If you were to write a switch statement, to 'pattern match' against types, what will u match against (the switch is on the TYPE not the value), something like this (not necessarily valid code) :

switch (typeof (x)) {
    case Just<a> : ...
    case Nothing<a> : ...
    default : ... // Here you dont have any 'a' to get the inner type
}
RKS
  • 309
  • 1
  • 6