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.
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.
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
.
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.
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.
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.
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.
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!