130
votes

I have scoured the internet for an actual explanation of what this keyword does. Every Haskell tutorial that I have looked at just starts using it randomly and never explains what it does (and I've looked at many).

Here's a basic piece of code from Real World Haskell that uses Just. I understand what the code does, but I don't understand what the purpose or function of Just is.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

From what I have observed, it is related to Maybe typing, but that's pretty much all I have managed to learn.

A good explanation of what Just means would be very much appreciated.

5

5 Answers

232
votes

It's actually just a normal data constructor that happens to be defined in the Prelude, which is the standard library that is imported automatically into every module.

What Maybe is, Structurally

The definition looks something like this:

data Maybe a = Just a
             | Nothing

That declaration defines a type, Maybe a, which is parameterized by a type variable a, which just means that you can use it with any type in place of a.

Constructing and Destructing

The type has two constructors, Just a and Nothing. When a type has multiple constructors, it means that a value of the type must have been constructed with just one of the possible constructors. For this type, a value was either constructed via Just or Nothing, there are no other (non-error) possibilities.

Since Nothing has no parameter type, when it's used as a constructor it names a constant value that is a member of type Maybe a for all types a. But the Just constructor does have a type parameter, which means that when used as a constructor it acts like a function from type a to Maybe a, i.e. it has the type a -> Maybe a

So, the constructors of a type build a value of that type; the other side of things is when you would like to use that value, and that is where pattern matching comes in to play. Unlike functions, constructors can be used in pattern binding expressions, and this is the way in which you can do case analysis of values that belong to types with more than one constructor.

In order to use a Maybe a value in a pattern match, you need to provide a pattern for each constructor, like so:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

In that case expression, the first pattern would match if the value was Nothing, and the second would match if the value was constructed with Just. If the second one matches, it also binds the name val to the parameter that was passed to the Just constructor when the value you're matching against was constructed.

What Maybe Means

Maybe you were already familiar with how this worked; there's not really any magic to Maybe values, it's just a normal Haskell Algebraic Data Type (ADT). But it's used quite a bit because it effectively "lifts" or extends a type, such as Integer from your example, into a new context in which it has an extra value (Nothing) that represents a lack of value! The type system then requires that you check for that extra value before it will let you get at the Integer that might be there. This prevents a remarkable number of bugs.

Many languages today handle this sort of "no-value" value via NULL references. Tony Hoare, an eminent computer scientist (he invented Quicksort and is a Turing Award winner), owns up to this as his "billion dollar mistake". The Maybe type is not the only way to fix this, but it has proven to be an effective way to do it.

Maybe as a Functor

The idea of transforming one type to another one such that operations on the old type can also be transformed to work on the new type is the concept behind the Haskell type class called Functor, which Maybe a has a useful instance of.

Functor provides a method called fmap, which maps functions that range over values from the base type (such as Integer) to functions that range over values from the lifted type (such as Maybe Integer). A function transformed with fmap to work on a Maybe value works like this:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

So if you have a Maybe Integer value m_x and an Int -> Int function f, you can do fmap f m_x to apply the function f directly to the Maybe Integer without worrying if it's actually got a value or not. In fact, you could apply a whole chain of lifted Integer -> Integer functions to Maybe Integer values and only have to worry about explicitly checking for Nothing once when you're finished.

Maybe as a Monad

I'm not sure how familiar you are with the concept of a Monad yet, but you have at least used IO a before, and the type signature IO a looks remarkably similar to Maybe a. Although IO is special in that it doesn't expose its constructors to you and can thus only be "run" by the Haskell runtime system, it's still also a Functor in addition to being a Monad. In fact, there's an important sense in which a Monad is just a special kind of Functor with some extra features, but this isn't the place to get into that.

Anyway, Monads like IO map types to new types that represent "computations that result in values" and you can lift functions into Monad types via a very fmap-like function called liftM that turns a regular function into a "computation that results in the value obtained by evaluating the function."

You have probably guessed (if you have read this far) that Maybe is also a Monad. It represents "computations that could fail to return a value". Just like with the fmap example, this lets you do a whole bunch of computations without having to explicitly check for errors after each step. And in fact, the way the Monad instance is constructed, a computation on Maybe values stops as soon as a Nothing is encountered, so it's kind of like an immediate abort or a valueless return in the middle of a computation.

You Could Have Written Maybe

Like I said before, there is nothing inherent to the Maybe type that is baked into the language syntax or runtime system. If Haskell didn't provide it by default, you could provide all of its functionality yourself! In fact, you could write it again yourself anyway, with different names, and get the same functionality.

Hopefully you understand the Maybe type and its constructors now, but if there is still anything unclear, let me know!

45
votes

Most of the current answers are highly technical explanations of how Just and friends work; I thought I might try my hand at explaining what it's for.

A lot of languages have a value like null that can be used instead of a real value, at least for some types. This has made a lot of people very angry and been widely regarded as a bad move. Still, it's sometimes useful to have a value like null to indicate the absence of a thing.

Haskell solves this problem by making you explicitly mark places where you can have a Nothing (its version of a null). Basically, if your function would normally return the type Foo, it instead should return the type Maybe Foo. If you want to indicate that there's no value, return Nothing. If you want to return a value bar, you should instead return Just bar.

So basically, if you can't have Nothing, you don't need Just. If you can have Nothing, you do need Just.

There's nothing magical about Maybe; it's built on the Haskell type system. That means you can use all the usual Haskell pattern matching tricks with it.

14
votes

Given a type t, a value of Just t is an existing value of type t, where Nothing represents a failure to reach a value, or a case where having a value would be meaningless.

In your example, having a negative balance doesn't make sense, and so if such a thing would occur, it is replaced by Nothing.

For another example, this could be used in division, defining a division function that takes a and b, and returns Just a/b if b is nonzero, and Nothing otherwise. It's often used like this, as a convenient alternative to exceptions, or like your earlier example, to replace values that don't make sense.

2
votes

A total function a->b can find a value of type b for every possible value of type a.

In Haskell not all functions are total. In this particular case function lend is not total - it is not defined for case when balance is less than reserve (although, to my taste it would make more sense to not permit newBalance to be less than reserve - as is, you can borrow 101 from a balance of 100).

Other designs that deal with non-total functions:

  • throw exceptions upon checking input value does not fit the range
  • return a special value (primitive type): favourite choice is a negative value for integer functions that are meant to return Natural numbers (for example, String.indexOf - when a substring is not found, the returned index is commonly designed to be negative)
  • return a special value (pointer): NULL or some such
  • silently return without doing anything: for example, lend could be written to return old balance, if the condition for lending is not met
  • return a special value: Nothing (or Left wrapping some error description object)

These are necessary design limitations in languages that cannot enforce totality of functions (for example, Agda can, but that leads to other complications, like becoming turing-incomplete).

The problem with returning a special value or throwing exceptions is that it is easy for the caller to omit handling of such a possibility by mistake.

The problem with silently discarding a failure is also obvious - you are limiting what the caller can do with the function. For example, if lend returned old balance, the caller has no way of knowing if balance has changed. It may or may not be a problem, depending on the intended purpose.

Haskell's solution forces the caller of a partial function to deal with the type like Maybe a, or Either error a because of the function's return type.

This way lend as it is defined, is a function that doesn't always compute new balance - for some circumstances new balance is not defined. We signal this circumstance to the caller by either returning the special value Nothing, or by wrapping the new balance in Just. The caller now has freedom to choose: either handle the failure to lend in a special way, or ignore and use old balance - for example, maybe oldBalance id $ lend amount oldBalance.

-1
votes

Function if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a) must have the same type of ifTrue and ifFalse.

So, when we write then Nothing, we must use Maybe a type in else f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type