Refactor imperative code to a single composed expression using Box
For this tutorial, we’re going to look at another example of the abstraction
Box( ) (see video2). All of the tutorials, including the code examples, are available from Github. So if you read something that you feel could be explained better, or a code example that needs refactoring, then please let me know via a comment or send me a pull request.
Rewind — function composition with ordinary functions
Before we cover this week’s code example, let’s step back for a moment to talk more about function composition. While Brian covered it in his video1, there’s been good feedback that I glossed over it in Tutorial 1. So here goes:
You should be asking “why is mapping over Box better than using ordinary functions (a -> b)?” Well, it’s not always better because ordinary functions are simpler to read, write, use, and you can reason about their behavior more quickly. For example, it is easier to do the following in PureScript:
function x = foo $ bar $ baz $ quux $ x
or function x = foo <<< bar <<< baz <<< quux $ x
or function x = (quux >>> baz >>> bar >>> foo) x
Apply ($), backward composition (<<<), or forward composition (>>>) — they all return the same result. So choose which ever is easier for you to understand.
Now, using composition on ordinary functions, let’s refactor
nextCharForNumberString from tut01/src/Main.purs. I chose forward composition (>>>) because I prefer showing long transformation chains starting with the first transformation. I'll often put them on separate lines to make them more readable, and I prefer pointfree style (i.e., not mentioning the argument
str) whenever possible.
nextCharForNumberString :: String -> String
fromMaybe 0 >>>
(+) 1 >>>
Where Functors shine is when you’re mixing categories, and you need a bridge from one category to the other. First, what is a category? Well, we’ve seen two categories already from Tutorial 1.
Box is a category and
Maybe is another, and each contains our transformed values at some point during the transformation chain. If you recall, we passed the value from
Maybe back into
Box by using the
fromMaybe function. Still, using
Box and its instances
fold is contrived because
nextCharForNumberString composes just fine using ordinary functions.
But in the wild, you’ll often be mixing several categories, and thus you’ll likely need to provide an adaptor layer that transforms another category to yours or vice-versa. Functors help you to write this adaptor layer, but we’re not quite ready to show how. We just need a few more tools in our toolbox, so I’ll come back to this topic later in the tutorial series.
Taking Box out for another spin
Our Tutorial 2 code example solves the simple problem of computing a discount, given money and percentage strings. We’re going to use our Box functor again from Tutorial 1, but it would’ve been perfectly okay to use ordinary function composition. Since our objective is to learn the Box functor, let’s take it out for another spin.
Where did our Box declaration go?
You might be wondering what happened to our
Box Class and instances from Tutorial 1, or why aren't they declared in
Main.purs. Well, Box is all safe and warm, tucked away in tut02/src/Data/Box.purs. We're going to be using Box in several tutorials, so its best to create a module for it and park it somewhere permanent. In idiomatic PureScript (and Haskell) you'll see type constructors (e.g., List) and functions for working with these constructors in
src/Data. Then, just import the module (i.e.,
import Data.Box (Box(..)) as you would any other module when you need to call the constructor and its functions. I've added a couple more instance declarations to
Box.purs, but you can ignore them as we'll be sticking with
extract for now.
Convert the money string to float
const moneyToFloat = str =>
.map(s => s.replace(/\$/g, ''))
.map(r => parseFloat(r))
The PureScript example below is much more interesting:
moneyToFloat :: String -> Box Number
moneyToFloat str =
Box str #
map (replace (Pattern "$") (Replacement "")) #
map (\replaced -> unsafeParseFloat replaced)
replace function, notice the constructor
Pattern, which is used by the
purescript-strings module to match our substring. I chose to use a substring rather than a regular expression just to keep it simple. It's a viable solution because there should only be one dollar sign in the currency string. Our new substring “” is wrapped in a
Replacement constructor which specifies a replacement for our pattern. You can check out Pursuit for more information on these two constructors.
Let’s move on to
parseFloat function, invalid number strings return,
NaN. But we told the type system that we’re always returning a
Number - ouch! We can't return
NaN without warning the user because that’s not how we roll in PureScript. We must deal with it - no excuses!
In production, returning a
Either constructor here would be two good choices, so that the type signature makes it clear that the user must deal with the possibility of
NaN. We’ll cover both of these in detail in future tutorials. To avoid conflating the example with these abstractions, I chose to create
unsafeParseFloat (see Main.js). Here's why:
In PureScript and other functional languages, it’s a common idiom to designate functions as either
unsafe whenever there’s the possibility of side effects, such as exceptions. For example, you might create the foreign declaration
unsafeHead which returns the head of an array. And to deal with the possibility of calling
unsafeHead on an empty array, you can decide to throw an error exception. Therefore designating it with the
unsafe prefix warns the user that they should ensure that they never call
unsafeHead with an empty array.
Convert the discount string to a number
Next up is
percentToFloat which is nothing new, with one exception. We can start immediately with a
str.replace(/\%/g, '')). It means that in practice we don't have to put a value in the box first before applying our first transformation function. It just comes down to your preference for readability and performance.
const percentToFloat = str =>
.map(replaced => parseFloat(replaced))
.map(number => number * 0.01)
Now the equivalent in PureScript. Again, nothing new here, other than what was described above.
percentToFloat :: String -> Box Number
percentToFloat str =
Box (replace (Pattern "%") (Replacement "") str) #
map (\replaced -> unsafeParseFloat replaced) #
map (_ * 0.01)
And take it home with applyDiscount
savings for use within the final expression. But the big ‘ah-ha moment’ is that we use
fold instead of
map before applying the final expression. Had we used
map on each instead of
fold, the result would’ve been two boxes deep
const applyDiscount = (price, discount) =>
.fold(savings => cost - cost * savings))
map instead of
extract, the compiler will raise an error message, seeing as you’re calling
percentToFloat with a
Box String instead of
String. That’s the beauty of an intelligent type checker! I encourage you to go ahead and try this yourself so that you become familiar with PureScript's compiler error messages. Again, from Tutorial 1, be aware that
extract is different than
extract does not apply a function before taking the value out of the box. That’s why I’ve got
extract as a function of the string transformations, and its tucked inside parenthesis instead of chaining.
applyDiscount :: String -> String -> Number
applyDiscount price discount =
(extract $ moneyToFloat price) #
(\cost -> (extract $ percentToFloat discount) #
(\savings -> cost - cost * savings))
One final note: besides using ordinary function composition (see Function Composition with ordinary functions above), there’s a more canonical approach to writing
applyDiscount. So for those of you who like to see what’s in store for my future tutorials, I’ve included this canonical pattern as a bonus example in the code.
safeParseFloat as the example.
foreign import unsafeParseFloat :: String -> Number
exports.unsafeParseFloat = parseFloat;
Pulp will find
I hope you appreciate the simplicity of PureScripts FFI. I believe it’s a real advantage over other FFI implementations such as ports in Elm.
That’s all for Tutorial 2. Until next time!