This post is a little addendum to the excellent Safely Accessing Deeply Nested Values In JavaScript by A. Sharif. Please read his post first, it has some useful things!
In this post I will describe how I think about the functional code from the user's perspective.
The Problem
How do you access nested properties in an object? Let us say we have an object with user comments and we want to get comments for the first post
1 | const props = { |
What happens if a deeply nested property does not exist? The program crashes and burns!
1 | console.log(props.users.posts[0].comments) |
Manual solution
So we need to check at every step if a property exists before accessing it. It gets tedious really really quickly. We can write a helpful utility function for safely getting a property. Just give this function a path and it will get you the deep value or a null if the path leads nowhere.
1 | const get = (path, object) => |
Hint: I am using comment-value to embed output values as source comments.
User friendly API design
Notice how user-friendly our function get
is because it puts the path
argument first and
object
argument second. I always give this advice when designing the order of
function's arguments
Place the argument most likely to be known first on the left
This advice plays very nicely with the built-in JavaScript
partial application mechanism.
We are likely to know the path
we want to access before we get the actual object
.
Thus we can create a nice intermediate function for getting us the nested value in a
particular case.
1 | const pathToComments = ['user', 'posts', '0', 'comments'] |
Even friendlier API design
If we need to use the expression get.bind(null, ...)
to create an intermediate access
function every time we need it we start complaining. And we can complain to the library's
authors and have them embed the partial application inside the get
function itself.
JavaScript functions are first class values, so we can easily split
the get
definition to accept its two arguments path
and object
in two calls. The change
is so small with fat arrows functions, I will only show before and after 😀
1 | const get = (path, object) => // before |
Tiny change, yet it makes the user's life so much simpler.
1 | const firstPostComments = get.bind(null, pathToComments) // before |
The savings in code length AND readability are now accruing every time someone calls get
to create an intermediate deep access function, while there was no increase in complexity to
the get
code. We could go one step further and curry get
using Ramda.curry function.
1 | const {curry} = require('ramda') |
The benefit of using 3rd party library to curry function, is that we can provide any number of arguments at once.
1 | // provide both arguments together! |
Nice!
Default value
If we have started designing a "get" function to be user-friendly,
we can think about other common use cases.
For example, what happens if the value is not there? We just return null
, which looks bad.
The user can specify an alternative using built-in JavaScript OR operator ||
,
but that again means it has to be done at the caller site.
1 | console.log(firstPostComments(props) || 'no comment') //> ["Good one!","Interesting..."] |
We cannot partially apply the built-in OR operator, right? Otherwise we could write something like (well, if the partial application were done from the right)
1 | const ||NoComment = ||.bind(null, 'no comment') |
But we have to live within the standard JavaScript, so we have to find a way to specify the
default value inside the get function. This is similar to how we added the partial
application inside the function - we have noticed a common client use case and implemented
it in the function itself by using curry
.
We can go several ways about providing the default value.
The simplest is to let the user specify the default value
right inside the get
function. Again we have to think about the signature design. When are
we likely to know the default value to return? Consider the choices derived from the use cases
1 | get('no comment', path, object) // 1 |
If we go with approach // 3
, we have to provide the default value after the object, which
means specifying it every time. While flexible, it quickly becomes verbose, and there is no
built-in way to partially apply the right argument (there are library functions we could use
like Ramda.partialRight)
1 | console.log(firstPostComments(props, 'no comment')) //> ["Good one!","Interesting..."] |
If we place the default value as the first or second argument in get
signature, we can
easily apply it. To me these two positions seem equivalent. Let us place the default value
at first position, so the user has to think about "unhappy path" right away.
1 | const get = curry((defaultValue, path, object) => |
Perfect. Yet we now have a question of what the function get
does. On one hand it goes through
the object to grab the deeply nested value.
On the other hand, it also returns the default value if the path does not exist,
or the resulting value is falsy. This breaks my heuristic for determining when a code fragment
grows too large:
You have to use words like "AND / OR" to describe what the code does
In our case, the "OR" functionality can be thought as the second step that runs after the original
get
returns a value from an object. Let us write or
and use it to return the default value
if the original get
(now renamed to _get
) returns nothing.
1 | const _get = (path, object) => |
When designing or
we should follow the same API design philosophy as designing get
function.
The typical use cases should determine the order of arguments, and if necessary, the built-in
application. Notice how we place the default value on the left, because we are likely to know
it early. I am not going to completely rewrite the above or
, but that is definitely possible
and would be equivalent to Ramda.either (Ramda.or is meant to cast values
into Booleans).
Executing code based on returned value
So far, we have grabbed the nested property and printed it. We have even built-in a way to print default value if the nested property does not exist. What if we want to run multiple commands but only if the value exists? Hmm, we now have two problems:
- It is harder to tell when the property is invalid. Used to be easier: if the returned value is
undefined
ornull
then it meant the property is invalid. Now we need to compare to the default value, which is trickier. - We have to use JavaScript
if (...) { ... }
construct, which is NOT composable.
Here is an example the explains the point # 2. Imagine we want to run foo()
and bar()
if the property is present. It is easy to do right after the property access.
1 | const foo = () => console.log('in foo') |
What if we want to run more functions if there is a property?
We have to code them right there inside the
if (firstPostComments(props) !== 'no comment') { ... }
block. Alternative - return the fetched
property from the code and have the check repeat outside.
1 | const foo = () => console.log('in foo') |
This code is bad, and we need to solve both problems at once. We need a way to "flag"
the situation when the original property was invalid to avoid comparing it to
no comment
value everywhere, and we need a way to "queue up" functions to execute
if the returned property is valid.
Enter Maybe
Let us solve all problems by returning an object of Maybe type instead of
a "plain" value. This object is like a box - it can keep anything inside; the box does not care.
But what it can also do is "know" if the value inside is "valid" or "bad". We will flag a
value when creating the box (values that are undefined
or null
are called "nullable" and
that tells the Maybe
box that they are "bad" on creation)
1 | const Maybe = require('data.maybe') |
The get
definition just gets the value and then returns it wrapped in "Maybe" box. We can
shorten this a little bit using equivalent code (or not) depending on the mood.
One thing that we can notice right away is that the value we print are not "simple".
Instead they are objects that know if the value inside is bad (property #type:Maybe.Nothing
) or
good (same property is Maybe.Just
)
1 | const pathToComments = ['user', 'posts', '0', 'comments'] |
Cool things we can do now: we ca use the response "box" and run new functions easily if and only if the value inside is good. Printing a good value (and doing nothing for the bad one) is simply:
1 | firstPostComments(props).map(console.log) |
If we get the comments deep somewhere inside our code, we can simply return the Maybe
box
and let the other code do something if the value is good.
1 | function getComments() { |
This is why people say
that wrapped values (like Maybe
monad) are great for composability; because you can compose multiple functions easily using them.
Single vs multiple null checks
So far we have created a single Maybe
after getting the deeply nested property. But if we
look inside the get
function we see the same pattern inside the reduce
callback
1 | const _get = (path, object) => |
Hmm, this looks like our Maybe
box! Maybe.fromNullable
puts value in the Maybe
box,
and then lets Maybe
decide: if the value is good, calls a function, which constructs another box.
1 | const get = (path, object) => |
Yet there is a tiny difference, which creates a problem and does not give us a valid result
1 | const pathToComments = ['user', 'posts', '0', 'comments'] |
We have to pass a function to Maybe.map
that returns
not a simple value, but another Maybe
box. If we just use map
we quickly get into a
situation where boxes are nested inside other boxes, just like a Russian
Matryoshka doll.
1 | console.log(Maybe.fromNullable('foo') |
In our case, the first iteration of get(..., props)
fetches "user" value, which returns
Maybe{ Maybe {...} }
and then it tries to fetch "posts", which does not exist on the
Maybe
itself. Thus it catches it and returns just nothing.
We need to deal with this somehow. We cannot use map
because we return an already wrapped
value by checking inside Maybe.fromNullable
.
The wrapped types like Maybe
thus implement a separate method to call functions that return
wrapped results. In data.maybe
case it is chain
- a wrapped value returned from the chained callback
replaces the value in the current box.
1 | console.log(Maybe.fromNullable('foo') |
Let us replace map
with chain
inside our get
function and all should work out
1 | const get = (path, object) => |
Composing chains
Notice how we have replaced a ternary operator (xs && xs[x]) ? xs[x] : null
inside the reduce
callback (and the outside Maybe.fromNullable
) with a Maybe.chain()
function? Nice!
Having a function call instead of the built-in JavaScript ternary operator allows us to
simplify it (just like we simplified code when we replaced built-in ||
operator).
I am going to simplify get
in several steps for clarity. Let us first replace box.chain
with
Ramda.chain called on a box
. It is a good idea to write a few unit tests before
refactoring code like this.
1 | // before |
Second, let us use a function call to get a property of an object, rather than using built-in
object[name]
syntax. In our case, we can use Ramda.prop function.
1 | // use Ramda.prop instead of xs[property] |
Then we can abstract nested function calls Maybe.fromNullable(R.prop(property, xs)
into its own function.
1 | // extract safe property access |
Hmm, if we have safeProp
, maybe we do not need to always use get
. In a simple case, like
grabbing a value a few levels deep, we can just chain it ourselves.
1 | Maybe.fromNullable(props) |
Looks awful 🤔
Luckily, there is utility methods for composing wrapped values that have chain interface. In Ramda it is composeK and pipeK functions.
1 | const getComments = R.pipeK( |
In order to protect against the missing or undefined first value, just put the protection function first. It is that easy with functional programming.
1 | const getComments = R.pipeK( |
Safe and sound.
Extra info
- User friendly API
- If Else vs Either Monad vs FRP
- List of awesome functional resources
- Put callback first for elegance
if there is another resource that might be helpful, leave a comment.