Hacker News new | ask | show | jobs
by tikhonj 4807 days ago
This article makes it seem like you'd have to explicitly check whether a Maybe value is Nothing when you use it. This is certainly safe, but it's also very awkward; as a contrived example, adding two numbers would look like this:

    case a of
      Nothing -> Nothing
      Just a  -> case b of
        Nothing -> Nothing
        Just b  -> a + b
This is quite a bit of boilerplate hiding the expression that actually matters--a + b! Moreover, whenever you have code that creeps steadily to the right, it means you either messed up or missed an abstraction.

It turns out that this pattern--do a computation if all the values are present, but return Nothing if any of them are Nothing--happens very often. Happily, we can get some nice syntax for Maybe computations like this using do-notation in Haskell or for-comprehensions in Scala:

    do a <- a
       b <- b
       return (a + b)
This is much better! It makes even more sense for more complicated expressions, especially when later results depend on values of earlier ones. However, for a simple example like a + b, it's still quite a bit of boiler plate; we can certainly do better! Here are two alternatives using functions from the Control.Applicative module:

    (+) <$> a <*> b
    liftA2 (+) a b
The important idea here is that both versions somehow "lift" the (+) function to work over Maybe values. This just means they create a new function with checks for Maybe built in. This is great because it saves all the boilerplate above and nicely abstracts away most of the null checks while preserving safety. But the syntax is still a bit awkward. Happily, if you don't mind using a preprocessor[1], you can get some very nice syntax called "idiom brackets":

    (| a + b |)
[1]: https://personal.cis.strath.ac.uk/conor.mcbride/pub/she/

The computation inside the (| and |) is lifted over Maybe, just like the two previous examples. I think this is the clearest option here: it has the least syntactic overhead, and the base expression--a + b--is very easy to read. They also have the advantage of nesting, so you can express a + b + c, where you want a null check for all three variables, as:

    (|(|a + b|) + c|)
This isn't perfect, but I think it's still very easy to follow. It might be better if the (| and |) were a single character, something like this:

    ⦇⦇a + b⦈ + c⦈
However, some people really don't like Unicode symbols in their code :(. Happily, you can have the source look like (|foo|) and have Emacs replace it with ⦇foo⦈ without actually changing the code. It's basically Unicode syntax highlighting. I think this leads to the most readable code so far.

So my main point is that you can abstract out the common case where you check for Nothing and make the whole expression Nothing if any sub-expression is. This saves quite a bit of typing and much more importantly makes the resulting code far easier to read.

Another really cool part is that all these syntax forms and functions are not specific to Maybe--they actually work for a whole bunch of different types. So you would not be bloating your language by including special features just for safely checking nulls; these features are much more general.

3 comments

You can also make Maybe an instance of various typeclasses for even nicer syntax:

    instance Num a => Num (Maybe a) where
        (+) = liftM2 (+)
        (-) = liftM2 (-)
        (*) = liftM2 (*)
        abs = liftM abs
        signum = liftM signum
        negate = liftM negate
        fromInteger = Just . fromInteger

    > Just 4 + 2 * Just 6
    Just 16
    > Nothing * 42
    Nothing
Notice how the fromInteger method allows you to freely mix Maybe and non-Maybe numbers.
I don't have a whole lot of Haskell experience ... but these "Maybe" unwrapping functions are rare right? Like a null check I'd think that most of the time you make the check when you get the unreliable input or allocation or whatever, and thereafter in the guts of you program you have the certainty that the input has been "checked".
The real trick is that, conceptually, these are not really "unwrapping" functions--instead, they're "propagating" functions. All they do is take Maybe values from deep inside your computation and string them through to the outside.

In practice, this means that you write a decent part of your program using these techniques, which creates a block of code that produces a Maybe value after taking a bunch of Maybe inputs. Then you only use a case statement at the very end, when you need to plug the Maybe value back into normal code.

All these functions are useful for one particular case: you don't know what to do with a Nothing value, so if you see one anywhere, you just pass it on: your final result is Nothing. That pattern just turns out to be very useful.

>That pattern just turns out to be very useful.

Like NaN, it can be hard to track down where things went wrong.

Except contrary to NaN, the type system encodes where it can exist and what its span is.
Right, you can rule out the pieces that are typed as "NaN-free" - hopefully that's a lot.

This is a great reason to avoid huge chunks of code stitched together staying inside Maybe, while still being convenient on the small scale.

But that's precisely why you abstract it; so you can switch to Error in need be...
> Like NaN, it can be hard to track down where things went wrong.

Unlike NaN, you have the freedom to not use it when you don't want it.

So, Either String a <=> Maybe a, but with a nice reason something went wrong.
Not quite. They are not restricted to error handling. You can have methods returning a Maybe something without it being an error (just like there are plenty of valid reasons for returning null instead of throwing an exception). You can also use Maybe in a data structure:

  data Employee = Employee { name : Text, spouse : Maybe Text }
You may also want to use Either to store two possible outcomes of an operation, though I would recommend using your own sum type for clarity:

  data MyOwnEither a b = MyLeft a | MyRight b
If Nothing is ever an error, than you can add code to handle that case. It isn't really different from some method returning an empty list and other functions being basically no-ops afterward.
Depends on the structure of your program. When the Maybe represents the return value of some function that might fail, like a "null" in another language, you'll probably try to eliminate the Nothing case fairly soon after getting it on, and if you have several to eliminate you might use the monad syntax to avoid repeated checks. (User input will commonly use an Either or Error rather than a Maybe, so that you have some error status for what went wrong, but the same principle applies.)

However, sometimes a Maybe represents an inherently "optional" part of your data model, such as "a Foo may have zero or one Bar". In that case, you'll probably hold onto the Maybe until the point where you'd actually read and use that field.

Yes, that's the great thing about Maybe — any code that doesn't need to care about the uncertainty is freed up from worrying about nulls by guaranteeing it won't get one. Most of your functions will usually deal with the unwrapped type, so if you try to pass the Maybe to these functions, the type-checker will say, "Wait, this function isn't expecting a Maybe. You've done something wrong here." So you have to do your "null check" at the point where you get your Maybe, and then you know for certain that the rest of your code won't explode with a NullPointerException or whatever.
They are pretty rare. Very few functions actually want to use nullable-arguments. Another reason why nullable by default is a bad idea.
Yes. This is perhaps the most important point in this thread. Monads and idioms are neat, but for Maybe's in most situations they are not really necessary since there is often a single point where you `case` on a Mabye and that's it.
Lots of things can give a maybe - lookup in a container, for instance - that might be lower down in your code.
Sounds like a bad idea to me. Even though Num doesn't have an explicit contract, we sort of expect it to behave "nicely".

But here we start with a nice ring like Integer and end up with a type that has this weird, extra element that has no inverse with respect to addition, etc.

For all fixed length two's complement integer types the smallest representable number has no additive inverse, too.
Prelude Data.Int> minBound + minBound :: Int16

0

Ohhh yes, of course, it is still a abelian group.
At least in Clojure, it's considered bad form to extend your own protocols (aka instance type classes) to types you don't own. Isn't that also true of Haskell?

Haskell's type classes, unlike ML functors (I think), are "coherent", which means that you can't have scoped or multiple instances for the same type, lest you risk breaking the type system. With that in mind, extending Maybe to Num would mean a reduction the number of type errors caught in other code that uses Maybe and Num near each other.

Defining instances for both someone else's class and someone else's type is considered bad and GHC will warn:

http://stackoverflow.com/questions/3079537/orphaned-instance...

There are occasions where it can be useful to have a module export an orphan instance for compatibility before it makes it into the more appropriate spot in the standard libs.

And you're right you can't have multiple instances for the same type; a newtype wrapper is required.

Ah yes, wonderfully clear syntax:

    do a <- a
       b <- b
That was a poor choice. The local “a” shadows the parameter “a”. You can of course write:

    addM :: Maybe Int -> Maybe Int -> Maybe Int
    addM ma mb = do
      a <- ma
      b <- mb
      return (a + b)
As an aside, the inferred type would be much more general, because (+) works on any number and do/return work on any monad:

    addM :: (Monad m, Num a) => m a -> m a -> m a
Which means you could use it like this:

    addM (Just 5) (Just 10) == Just 15
But also like this:

    addM [1, 2] [4, 8]
      == [1 + 4, 1 + 8, 2 + 4, 2 + 8]
      == [5, 9, 6, 10]
Or like this:

    addM (Right 42) (Left "NaN") == Left "NaN"
And in many more interesting ways. :)
Of course, addA = liftA2 (+) is even more general.
That particular syntax makes much more sense when you have functions that return a Maybe value:

    do a <- getA "foo" "bar"
       b <- getB "foo" a
       ...
I used a deliberately overly simple example so I could go on from do-notation--which many people are already familiar with--to applicatives and idiom brackets.

Besides, it looks like any normal program, except you're using <- to define variables rather than =. Can't see how it could be any clearer than that.

Except that what it looks like isn't actually what it's doing (made even clearer by the a <- a example).

I like the Maybe concept and non-nullable types; I just think being able to overload operators like "=" and ";" in C++ and Haskell is optimizing writability over readability and in most cases, readability is by far the more important attribute.

You can't overload = or ; in Haskell. What you can do is to easily write code over some sort of 'boxed' values, where the type of the box is given by type signatures, and more importantly is almost always quite clear from context.

In this case, think of Maybe as a box containing one or zero instances of a type. For Maybe

    do x <- maybeAnInt
       y <- maybeAnotherInt
       return (x+y)
Lists are 'boxed' values containing any number of elements

    do x <- [1,2]
       y <- [10,20]
       return (x+y)
The monadic semantics for lists means this returns the sum of each combination of values, namely [11,21,12,22]. This is essentially like a database join, which is why a monadic structure was used for LINQ.

For Promises

    do x <- intPromise
       y <- anotherIntPromise
       return (x+y)
This returns a new promise containing the sum of the result of two promises instead of using callbacks.

Essentially all these types live in a Maybe/List/Promise box. There are many more examples. The nice thing about monads is that the semantics of how these things work is abstract enough to allow a variety of interpretations, but constrained enough (by the Monad laws) that you get a nice intuition of how things work after using a few different instances.

But you're quite clearly NOT overloading "=" or ";". Instead of saying

    do
       a = getFoo x y z
       b = getBar p q
(which won't compile), you're saying

    do
       a <- getFoo x y z
       b <- getBar p q
Which is not an overloaded operator. In fact, it's not even meant to suggest equals (which in Haskell rather strictly means mathematical equality) but rather assignment ("=" in the expression "x = x + 1;").
The use of Haskell here was just for illustration; there's nothing stopping a programming language designer from having clear, convenient syntax for dealing with Maybe values.
Another way to go would be to use some functions that conceal the mechanics of Maybe. For instance:

    (fromMaybe 0 a) + (fromMaybe 0 b)
This will always produce a number and it's pretty clear why you'd get the one you'd get.
I don't agree that it's pretty clear. 0 is a perfectly valid outcome of an addition, and you can't count on being the last calculation in the chain.
Did you misunderstand?

    do x <- a
       y <- b
       return $ x + y
can result in Nothing if either a or b is Nothing, but

    (fromMaybe 0 a) + (fromMaybe 0 b)
will always result in a number, with 0 being used in place of Nothing. If only one of them is Nothing, you'll get the value of the other, the Nothing having been treated as 0.
No, I didn't misunderstand. 'Nothing' and 0 are not interchangeable.
You did misunderstand: you took a friendly illustration of an alternative way to use Maybe as some kind of absolutist degree. Well, put down your sword. Much of the time when you have Maybe you also have a sensible default value and you want to use that rather than live the rest of your life inside Maybe. fromMaybe is just a handy way to deal with that case.
> No, I didn't misunderstand. 'Nothing' and 0 are not interchangeable.

Nobody said they were. That's idiomatic Haskell for something like this in Java when unboxing Integers:

  return (a != null ? a : 0) + (b != null ? b : 0);
As you can see, there is no adding Nothing to integers.