When learning functional programming in JavaScript, it is very tempting to write code in a pointfree style as much as possible.
Writing functions in pointfree-style means you write a function without mentioning the input argument.
Consider this function that takes an object representing a person, and returns the full name:
function fullName (personObject) {return [personObject.firstName,personObject.middleName,personObject.lastName].filter(element => element).join(' ')}
const me = {firstName: 'Thai',lastName: 'P'}
fullName(me)// => 'Thai P'
Notice the names, **_personObject_**
and **_element_**
. We needed to give names to our input variables, so that we could process them to derive a result.
But do we really have to do that? What if we could create this function by piecing together smaller functions? For example, Ramda contains a bunch of utility functions that helps us do this:
**R.props**
turns an array of property names into a function that takes an object and returns an array of values:
const nameComponents = R.props(['firstName','middleName','lastName'])
nameComponents(me)// => [ 'Thai', undefined, 'P' ]
**R.filter**
takes a predicate and generates a function that can filter an array using that predicate. **R.identity**
is equivalent to element => element
.
const rejectEmpty = R.filter(R.identity)
rejectEmpty([ 'Thai', undefined, 'P' ])// => [ 'Thai', 'P' ]
**R.join**
takes a separator string, and generates a function that joins an array into into a single string with the separator in between.
const unwords = R.join(' ')
unwords([ 'Thai', 'P' ])// => 'Thai P'
So, our **fullName**
function can be written as a pipeline consisting of these three smaller functions:
const fullName = R.pipe(nameComponents,rejectEmpty,unwords)
Let’s inline them, so that when we read the definition of **fullName**
, we know exactly what’s going to happen:
const fullName = R.pipe(R.props([ 'firstName', 'middleName', 'lastName' ]),R.filter(R.identity),R.join(' '))
fullName(me)// => 'Thai P'
As you can see, now we no longer need to make up names like **_personObject_**
and **_element_**
. Naming things is a hard computer science problem. Creating functions this way helps reduce the need to name things!
Note: I am only using
ramda
as an example. You can also uselodash/fp
,sanctuary
, or write these functions yourself. The thing is that you don’t write functions yourself, but you compose (reuse) smaller functions to create larger functions.
You’re doing pointfree style when you are partially applying functions:
const multiply = (a, b) => a * bconst double = multiply.bind(null, 2)
or when you are calling a curried function:
const multiply = (a) => (b) => a * bconst double = multiply(2)
Either way, you ended up with a function that’s generated by another function.
Short answer: Not always, as it could obfuscate the JavaScript stack trace. Read on for more explanation.
Let’s say we have an array of blog comments:
let comments = [{author: { firstName: 'Thai', lastName: 'P' },text: 'I like functional programming!'},{author: {firstName: 'A',middleName: 'random',lastName: 'commenter'},text: 'Why?'},{author: { firstName: 'Thai', lastName: 'P' },text: 'Where should we begin?'}]
Now, I want to obtain a list of people who commented, sorted alphabetically.
I could come up with something like this:
function fullName (personObject) {return [personObject.firstName,personObject.middleName,personObject.lastName].filter(element => element).join(' ')}
function commenters (comments) {const nameList = comments.map(comment => fullName(comment.author))return Array.from(new Set(nameList)).sort()}
commenters(comments)// => [ 'A random commenter', 'Thai P' ]
With our knowledge of pointfree-style, we could refactor the above into something like this:
const fullName = R.pipe(R.props([ 'firstName', 'middleName', 'lastName' ]),R.filter(R.identity),R.join(' '))
const commenters = R.pipe(R.map(R.pipe(R.prop('author'), fullName)),R.uniq,R.sortBy(R.identity))
This is much more concise and declarative!
Later, the comment system allows people to comment anonymously. This means the author
of the comment can now be **null**
.
comments = [ ...comments, {author: null,text: 'How about referential transparency?'} ]
Since we can’t read comment.author.firstName
, the app crashed.
Let’s now compare the stack traces between the two versions…
Uncaught TypeError: Cannot read property 'firstName' of nullat fullName (app1.js:4)at comments.map.comment (app1.js:15)at Array.map (<anonymous>)at commenters (app1.js:15)at index2.html:35
In this version, the stack trace above showed exactly where the error occurred: In the fullName
function called from commenters
’ mapping function passed to comments.map
.
No red squiggly lines here? But my app’s code lives here!
Uncaught TypeError: Cannot read property 'firstName' of nullat props (ramda.js:7314)at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at ramda.js:2061at ramda.js:205at _map (ramda.js:572)at map (ramda.js:848)at ramda.js:470at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at index2.html:35
In this version, all we see is Ramda.
Our application code (app2.js) is never mentioned in the stack trace. Indeed, we never created any function in that file. We just composed Ramda functions together, perhaps in a wrong way…
In this example, we used R.props
only once, so we know where to look. But what if the app is larger and R.props
is used at many places?
Have fun figuring that out!
Note: Again, this is not a problem with Ramda (or other FP libraries). We just used it to generate functions to make our code more declarative and pointfree, so Ramda is cool. What’s not cool is writing pointfree code without considering its impact on the stack trace.
trace
functionWhen concerns about debugging are raised, functional programming advocates would suggest dealing with this problem using a trace()
function:
const trace = text => (value => (console.log(text, value), value))
This allows us to see the values that flowed through a pipeline:
const pipeline = R.pipe(trace('input'),f,trace('after f'),g,trace('after g (output)'))
So let’s go ahead and put those trace
calls into our app!
const fullName = R.pipe(trace('fullName - input'),R.props([ 'firstName', 'middleName', 'lastName' ]),trace('fullName - after getting components'),R.filter(R.identity),trace('fullName - after filtering'),R.join(' '),trace('fullName - output'))
const commenters = R.pipe(trace('commenters - input'),R.map(R.pipe(trace('commenters - item - input'),R.prop('author'),trace('commenters - item - before fullName'),fullName,trace('commenters - item - after fullName'))),trace('commenters - raw list of authors'),R.uniq,trace('commenters - after unique'),R.sortBy(R.identity),trace('commenters - after sort (output)'))
Now, this allows us to see the cause of errors more easily:
Ok, so in **_commenters_**
we tried to send null into fullName
While this debugging technique works during development, it’s not practical for apps running on production, like, on the customer’s browser. Would you leave lot of tracing code like this in a production app?
Unexpected errors on production usually occur on customer’s browser (otherwise, our tests would have caught them), so now, all we have left is an error report with the stack trace. We can’t just tell our customer to edit our app’s source code and put in the trace()
calls!
When something goes wrong, the error report should contain enough information for developers to fix it. This is especially important if that error could not be reproduced deterministically.
Sure, this problem could have been better prevented had we have better test suites that covers all the edge cases. But unexpected things happen anyway. Being able to easily deal with unexpected errors on production is something that should not be overlooked.
When I learned functional programming from many sources, everyone marveled at how you can use curried functions to make functions snap together like lego pieces.
They say that **const** g = (x) => f(x)
is equivalent to just **const** g = f
. But no one ever mentioned about how, in practice, this could eliminate a stack frame that’s crucial for tracking the source of error.
How can we make our functional code easier to debug? How can we make our stack trace more meaningful?
We can create non-pointfree functions that calls the pointfree functions where appropriate:
const fullNamePipeline = R.pipe(R.props([ 'firstName', 'middleName', 'lastName' ]),R.filter(R.identity),R.join(' '))
function fullName (personObject) {return fullNamePipeline(personObject)}
const commentersPipeline = R.pipe(R.map(R.pipe(R.prop('author'), fullName)),R.uniq,R.sortBy(R.identity))
function commenters (comments) {return commentersPipeline(comments)}
Now our stack trace is a bit more meaningful:
Uncaught TypeError: Cannot read property 'firstName' of nullat props (ramda.js:7314)at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at fullName (app2.js:9)at ramda.js:2061at ramda.js:205at _map (ramda.js:572)at map (ramda.js:848)at ramda.js:470at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at commenters (app2.js:19)at index.html:35
But now there’s more code.
We can inject a stack frame by using this helper function. It helps injecting an artificial stack frame when called:
// Note: This function only works correctly in Chrome.
function /* yourNameIs */ 名は (名, f) {const キー = `(╯°□°)╯︵ ${名}`return {[キー] () { return f.apply(this, arguments) }}[キー]}
Then we can use it like this:
const fullName = 名は('fullName', R.pipe(R.props([ 'firstName', 'middleName', 'lastName' ]),R.filter(R.identity),R.join(' ')))
const commenters = 名は('commenters', R.pipe(R.map(R.pipe(R.prop('author'), fullName)),R.uniq,R.sortBy(R.identity)))
The stack trace now looks like this:
Uncaught TypeError: Cannot read property 'firstName' of nullat props (ramda.js:7314)at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at (╯°□°)╯︵ fullName (util.js:3)at ramda.js:2061at ramda.js:205at _map (ramda.js:572)at map (ramda.js:848)at ramda.js:470at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at (╯°□°)╯︵ commenters (util.js:3)at index.html:35
Our function is more clearly seen in the stack trace, but now we lost the information about where our function is created (app2.js).
We can improve our 名は
function by making it record the call site when we try to name a function:
// Note: This function only works correctly in Chrome.
function 名は (名, f) {const atOrigin = (String(new Error('ヤバい!').stack).split('\n')[2] || '').trim()const キー = `(╯°□°)╯︵ ${名} (created ${atOrigin})`return {[キー] () { return f.apply(this, arguments) }}[キー]}
Now our stack trace looks like this:
Uncaught TypeError: Cannot read property 'firstName' of nullat props (ramda.js:7314)at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at (╯°□°)╯︵ fullName (created at app2.js:2:18) (util.js:5)at ramda.js:2061at ramda.js:205at _map (ramda.js:572)at map (ramda.js:848)at ramda.js:470at ramda.js:138at f1 (ramda.js:31)at ramda.js:2061at ramda.js:2061at ramda.js:205at (╯°□°)╯︵ commenters (created at app2.js:8:20) (util.js:3)at index.html:35
Now this tells us not only the name of the function, but also where it got created.
This is just an idea… but would it be great if FP libraries could inject auxiliary stack frames for us where appropriate? When an error occurs, maybe the stack trace could look like this:
Uncaught TypeError: Cannot read property 'firstName' of nullat props (ramda.js:7314:19)at ramda.js:138:46at R.props([firstName,middleName,lastName]) (ramda.js:31:17)at ramda.js:2061:27at ramda.js:2061:27at R.pipe(R.props([firstName,middleName,lastName]),R.filter(R.identity),R.join(' ')) (ramda.js:204:43)at ramda.js:2061:14at R.pipe(R.prop(author),fullName) (ramda.js:204:43)at _map (ramda.js:572:19)at map (ramda.js:848:14)at ramda.js:470:15at ramda.js:138:46at R.map(R.pipe(R.prop(author),fullName)) (ramda.js:31:17)at ramda.js:2061:27at ramda.js:2061:27at R.pipe(R.map(R.pipe(R.prop(author),fullName)),R.uniq,R.sortBy(R.identity)) (ramda.js:204:43)at index2.html:43:13
This would make it much easier for developers to debug production problems.
Languages like Haskell and Elm helps prevent runtime exceptions at the type level. This means you could write pointfree functions and be confident that it will never receive unexpected values.
Pointfree-style codes in these languages are often seen. They’re even considered a ‘good discipline.’ Quoting the Haskell wiki:
This style is particularly useful when deriving efficient programs by calculation and, in general, constitutes good discipline. It helps the writer (and reader) think about composing functions (high level), rather than shuffling data (low level).
By the way, have fun dealing with JSON decoders and those Maybes! These safety features can make your code very verbose when it comes to dealing with data from external services, but this could be well worth it in the long run.
In my experience, refactoring Elm code is much more fun than refactoring JavaScript code, as the compiler helped me at every step.
function fullName (personObject) {return [ 'firstName', 'middleName', 'lastName' ].map(key => personObject[key]).filter(R.identity).join(' ')}function commenters (comments) {const nameList = comments.map(comment => comment.author).map(fullName)return R.sortBy(R.identity, R.uniq(nameList))}
Writing functional code in pointfree style can make your code more concise and declarative. There are less things to name. But it also comes with a cost. You no longer get stack trace clarity for free. Unless handled carefully, it could make your stack trace very obscure, making it hard to track down the source of errors in production apps.
Have more ideas on how to improve debuggability in functional JavaScript code? Please write a response! :)