Skip to content

Instantly share code, notes, and snippets.

@davidchambers
Created June 2, 2016 21:34
Show Gist options
  • Save davidchambers/6c03360592d4aa612f8f0909fe2d4c78 to your computer and use it in GitHub Desktop.
Save davidchambers/6c03360592d4aa612f8f0909fe2d4c78 to your computer and use it in GitHub Desktop.
Quick introduction to chaining monads from a pull request review
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:

  • mMaybe
  • aString
  • bNumber

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. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment