var convertPercentage = function(percentage) { if (percentage == null) { return null; } else { return parseFloat(percentage.replace(/[^-\d.]/g, '')); } };
Writing functions which have a special case for null
/undefined
is undesirable for several reasons:
- it increases the amount of code which must be written and maintained;
- it increases the number of code branches for which tests must be written; and
- it allows errors to propagate silently.
Let's start by considering the types. We expect the input to be a string, so let's require that. We then have:
convertPercentage :: String -> ???
See http://sanctuary.js.org/#types for an explanation of the notation.
So, what should the ???
be? We could make it Number
, but then we're forced to live with the fact that NaN
is a possible return value. NaN
—like null
—is problematic as it forces the caller to check the return value before using it in future computations.
The correct return type is Maybe Number
. The Maybe type is defined as:
data Maybe a = Just a | Nothing
So a value of type Maybe Number
is either a Just containing a number or it is Nothing. Regardless of which it is, we have a safe way to perform future computations on the value without first inspecting it.
In Haskell:
Prelude> fmap (+ 1) (Just 42)
Just 43
Prelude> fmap (+ 1) Nothing
Nothing
In JavaScript (with Ramda and Sanctuary):
> R.map(S.inc, S.Just(42))
Just(43)
> R.map(S.inc, S.Nothing())
Nothing()
So, we'll make the function's type:
convertPercentage :: String -> Maybe Number
Now, let's implement it:
// convertPercentage :: String -> Maybe Number
const convertPercentage = S.compose(S.parseFloat, R.replace(/[^-\d.]/g, ''));
convertPercentage('~42~')
will evaluate to Just(42)
while convertPercentage('XXX')
will evaluate to Nothing()
.
Now, let's revisit the null
problem. Here's the expression at the call site:
convertPercentage(R.path(['StandardPurchaseAPR', 'value'], balances))
The problem is that R.path
does not have the desired type; the function assumes that the path will always exist. Instead of using R.path
, let's use S.gets
:
// s :: Maybe String
const s = S.gets(String, ['StandardPurchaseAPR', 'value'], balances);
So, now we have a value of type Maybe String
which we wish to provide as an argument to a function of type String -> Maybe Number
. The types don't line up. What do you do? Enter R.chain
(which is the equivalent of Haskell's >>=
). It has the following type:
R.chain :: Monad m => (a -> m b) -> m a -> m b
Let's make this clearer by replacing the type variables as follows:
m
→Maybe
a
→String
b
→Number
This gives:
R.chain :: (String -> Maybe Number) -> Maybe String -> Maybe Number
convertPercentage
is of exactly the right type to use as the first argument to R.chain
! This gives:
R.chain(convertPercentage) :: Maybe String -> Maybe Number
So now we have a function of type Maybe String -> Maybe Number
, which is exactly what we wanted.
// s :: Maybe String
const s = S.gets(String, ['StandardPurchaseAPR', 'value'], balances);
// n :: Maybe Number
const n = R.chain(convertPercentage)(s);
Note that we were able to chain together two operations which may fail (nested property access and string parsing) without any error handling whatsoever. This is the beauty of monads. :)