Partially-applied (or curried) functions could obfuscate the JavaScript stack trace

2,224 reads

An oft-overlooked tradeoff when writing functional code in JavaScript.

When learning functional programming in JavaScript, it is very tempting to write code in a pointfree style as much as possible.

What is pointfree style?

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 use lodash/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 * b

const double = multiply.bind(null, 2)

or when you are calling a curried function:

const multiply = (a) => (b) => a * b

const double = multiply(2)

Either way, you ended up with a function that’s generated by another function.

But should we always use it?

Short answer: Not always, as it could obfuscate the JavaScript stack trace. Read on for more explanation.

An example…

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!

Then something unexpected happens

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…

Version 1 (not pointfree)

This file is where the error was thrown.

Uncaught TypeError: Cannot read property 'firstName' of null

at 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 .

Version 2 (pointfree)

No red squiggly lines here? But my app’s code lives here!

Uncaught TypeError: Cannot read property 'firstName' of null

at props (ramda.js:7314)

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at ramda.js:2061

at ramda.js:205

at _map (ramda.js:572)

at map (ramda.js:848)

at ramda.js:470

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at 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.

The trace function

When 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.

So what can we do?

How can we make our functional code easier to debug? How can we make our stack trace more meaningful?

Create a function yourself to establish a stack frame

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 null

at props (ramda.js:7314)

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at fullName (app2.js:9)

at ramda.js:2061

at ramda.js:205

at _map (ramda.js:572)

at map (ramda.js:848)

at ramda.js:470

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at commenters (app2.js:19)

at index.html:35

But now there’s more code.

Inject a stack frame dynamically

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 null

at props (ramda.js:7314)

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at (╯°□°）╯︵ fullName (util.js:3)

at ramda.js:2061

at ramda.js:205

at _map (ramda.js:572)

at map (ramda.js:848)

at ramda.js:470

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at (╯°□°）╯︵ 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).

Record the origin of a function

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('

')[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 null

at props (ramda.js:7314)

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at (╯°□°）╯︵ fullName (created at app2.js:2:18) (util.js:5)

at ramda.js:2061

at ramda.js:205

at _map (ramda.js:572)

at map (ramda.js:848)

at ramda.js:470

at ramda.js:138

at f1 (ramda.js:31)

at ramda.js:2061

at ramda.js:2061

at ramda.js:205

at (╯°□°）╯︵ 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.

Have libraries inject meaningful stack frames for us

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 null

at props (ramda.js:7314:19)

at ramda.js:138:46

at R.props([firstName,middleName,lastName]) (ramda.js:31:17)

at ramda.js:2061:27

at ramda.js:2061:27

at R.pipe(R.props([firstName,middleName,lastName]),

R.filter(R.identity),

R.join(' ')) (ramda.js:204:43)

at ramda.js:2061:14

at 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:15

at ramda.js:138:46

at R.map(R.pipe(R.prop(author),fullName)) (ramda.js:31:17)

at ramda.js:2061:27

at ramda.js:2061:27

at 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.

Use a typed language that guarantees that your functions will never receive an invalid data

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.

Or just don’t go overboard with pointfree style in JavaScript

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

}

Conclusion

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

Tags