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!
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.
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.
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:
lend
could be written to return old balance, if the condition for lending is not metThese 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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With