Reputation: 1471
I'm following "Real World Haskell", and am going through chapter 2, where the exercise is to create a "lastButOne" function which returns the second to last value in the list. My original code was:
lastButOne xs = if null (tail (tail xs))
then head xs
else lastButOne (tail xs)
Which works fine unless I give it a list with only 1 or 2 items. So I modified it to this:
lastButOne xs = if null xs || null (tail xs)
then head xs
else
if null (tail (tail xs))
then head xs
else lastButOne (tail xs)
Which checks if it only has 1 or 2 items, but then fails if it only has 1 item because of the call to head. My problem is that I can't think of anything else to return other than "head xs", ideally I want it to return null or something like that, but I can't find a "null" that allows the function to still be polymorphic.
Upvotes: 12
Views: 13827
Reputation: 3900
Perhaps you are looking for a Maybe
type:
lastButOne :: [a] -> Maybe a
lastButOne [x,_] = Just x
lastButOne (_:y:ys) = lastButOne (y:ys)
lastButOne _ = Nothing
Upvotes: 28
Reputation: 27227
[I]deally I want it to return null or something like that, but I can't find a "null" that allows the function to still be polymorphic.
There are a number of answers already in posted on how to make this work, so instead let me answer the more fundamental question of "Why isn't there a 'null'?"
This is part of the philosophy of the type-checker in Haskell. most languages include some form of a special value that signals an error or lack of data. Examples include 0
,-1
""
(the empty string) JavaScripts null
or Ruby's nil
. The problem is that this creates a whole class of potential bugs wherein the calling function only deals with "good" values and the program crashes or worse corrupts data when a special value is returned. There is also the issue of the special value being intended as a regular value -- that is 0
actually being a number and not a failure state.
In your problem, the question could be framed, "Would the function that calls lastButOne
know what to do with null
?"
The problem of not having a valid answer for certain otherwise well-formed inputs is a very common one in Haskell and has two typical solutions. The first is taken from Erlang: Fail quickly and cleanly. Under this solution it is the responsibility of the caller to ensure that the data is "good." If it is not the program dies with an error (or the exception is caught, but this is more difficult in Haskell then other languages) This is the solution used by most list functions e.g. head
, tail
, etc...
The other solution is to explicitly state that the function may fail to produce good data. The Maybe a
type is the simplest of these solutions. It allows you to return a value Nothing
that works much like the null
you are looking for. But there is a cost to this. Your function's type signature becomes lastButOne :: [a] -> Maybe a
. Now every calling function must take not an a
but a Maybe a
, and before it can get its answer, it must tell Haskell what it is going to do if the answer is Nothing
.
Upvotes: 10
Reputation: 13677
An example of "extended error handling" mentioned by @Andrew Jaffe could be:
{-# LANGUAGE FlexibleContexts #-} module Main where
import Control.Monad.Error
lastButOne :: MonadError String m => [a] -> m a
lastButOne (_ : y : []) = return y
lastButOne (_ : t) = lastButOne
lastButOne _ = throwError "Error in lastButOne"
In this case, it is possible to return Either
type: Right x
in case of success and Left "Error in lastButOne"
in case of failure, but many other instances of MonadError String m
are possible.
Also read 8 ways to report errors in Haskell for more possibilities.
Upvotes: 0
Reputation: 385
Why don't you use pattern matching to handle the case where you have only 1 or 2 elements? Something like:
lastButOne :: [a] -> Maybe a
lastButOne [x,y] = Just x
lastButOne (x:y:t) = lastButOne (y:t)
lastButOne _ = Nothing
Upvotes: 3
Reputation: 27097
Well, this depends on how much error-checking you actually want to do.
I suspect that, given this is only in Chapter 2, you are probably meant to ignore this edge case. Haskell itself throws an exception:
Prelude> head []
*** Exception: Prelude.head: empty list
The alternative, if this is a case that you will have to deal with a lot, would be to change the return type of lastButOne
to Maybe a
and return Just (head xs)
when it works, and Nothing
when it fails. But this means that you would always have deal with living inside the Maybe
monad, which may not be ideal (as you would have to "unwrap" the Just
value to use it).
Upvotes: 5