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 | NothingSo 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
NothingIn 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 NumberNow, 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 bLet's make this clearer by replacing the type variables as follows:
m→Maybea→Stringb→Number
This gives:
R.chain :: (String -> Maybe Number) -> Maybe String -> Maybe NumberconvertPercentage is of exactly the right type to use as the first argument to R.chain! This gives:
R.chain(convertPercentage) :: Maybe String -> Maybe NumberSo 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. :)