By now, you have everything you need to understand functional-light JavaScript. There's no more new concepts to introduce.
In this final chapter, our main goal is conceptual cohesiveness. We'll look at code that brings many of the major themes from this book together -- application of what we've learned. Above all, this example code is intended to illustrate the "Functional Light" approach to JavaScript -- that is, balance and pragmatism over dogma.
You'll want to practice these techniques yourself, extensively. Digesting this chapter is critical to helping you apply FP principles to your real world code.
Let's build a simple stock ticker widget.
Note: For reference, the entirety of the code for this example resides in the ch11-code/
sub directory -- see the GitHub repository for this book (https://github.com/getify/Functional-Light-JS). Also, selected FP helpers we've discussed throughout this book that we need for this example are included in ch11-code/fp-helpers.js
. In this chapter we will only focus on the relevant parts of the code for our discussion.
First, let's talk about the markup for this widget, so we have somewhere to display our information. We start out with an empty <ul ..>
element in our ch11-code/index.html
file, but while running, the DOM will be populated to look like:
<ul id="stock-ticker">
<li class="stock" data-stock-id="AAPL">
<span class="stock-name">AAPL</span>
<span class="stock-price">$121.95</span>
<span class="stock-change">+0.01</span>
</li>
<li class="stock" data-stock-id="MSFT">
<span class="stock-name">MSFT</span>
<span class="stock-price">$65.78</span>
<span class="stock-change">+1.51</span>
</li>
<li class="stock" data-stock-id="GOOG">
<span class="stock-name">GOOG</span>
<span class="stock-price">$821.31</span>
<span class="stock-change">-8.84</span>
</li>
</ul>
Before we go any further, let me remind you: interacting with the DOM is I/O, and that means side-effects. We can't eliminate these side effects, but we can limit and control them. We'll want to be really intentional about minimizing the surface area of our application that deals with the DOM. We learned all about these techniques in Chapter 5.
Summarizing our widget's functionality: the code will add the <li ..>
elements each time a new-stock event is "received", and will update the price and change as stock-update events come through.
In the Chapter 11 example code, in ch11-code/mock-server.js
, we set up some timers to push out randomly generated fake stock data to a simple event emitter, to simulate as if we were getting messages of stock information from a server. We expose a connectToServer()
function which pretends to do so, but really just returns the faked event emitter instance.
Note: This file is all fake/mock behavior, so I didn't spend much effort trying to make it very FP-adherent. I wouldn't suggest spending too much time concerned with the code in this file. If you wrote a real server -- a very interesting extra credit exercise for the ambitious reader! -- you'd clearly want to give that code the FP attention it deserves.
In ch11-code/stock-ticker-events.js
, we create some observables (via RxJS) hooked up to an event emitter object. We call the connectToServer()
to get this event emitter, then listen to the event names "stock"
(adding a new stock to our ticker) and "stock-update"
(updating the stock's listed price and change amount). Finally, we define transformations on the incoming data of these observables, formatting the data as needed.
In ch11-code/stock-ticker.js
, we define our UI (DOM side effect) behavior as methods on the stockTickerUI
object. We also define a variety of helpers, including getElemAttr(..)
, stripPrefix(..)
, and others. Finally, we subscribe(..)
to the two observables that provide us formatted data to render to the DOM.
Let's look at the code in ch11-code/stock-ticker-events.js
. We'll start with some basic helpers:
function addStockName(stock) {
return setProp( "name", stock, stock.id );
}
function formatSign(val) {
if (Number(val) > 0) {
return `+${val}`;
}
return val;
}
function formatCurrency(val) {
return `$${val}`;
}
function transformObservable(mapperFn,obsv){
return obsv.map( mapperFn );
}
These pure functions should be pretty straightforward to interpret. Recall setProp(..)
from Chapter 4 actually clones the object before setting the new property. That exercises the principle we saw in Chapter 6: avoiding side effects by treating values as immutable even if they're not.
addStockName(..)
is used to add a name
property to a stock message object that's equal to its id
. The name
value is later used as the visible stock name in the widget.
A little nuanced note on transformObservable(..)
: It's ostensibly pure in that map(..)
on an observable returns a new observable. But technically, under the covers, the internal state of obsv
is mutated to connect it to the new observable that map(..)
returns. This side effect isn't really a big deal and won't detract from our code readability, but it's important to spot side effects wherever they are, rather than being surprised by them when you encounter bugs!
When a stock message is received from the "server", it'll look like:
{ id: "AAPL", price: 121.7, change: 0.01 }
Prior to display in the DOM, the price
needs to be formatted with formatCurrency(..)
("$121.70"
), and the change
needs to be formatted with formatChange(..)
(`"+0.01"). But we don't want to mutate the message object, so we need a helper that formats the numbers and gives us a new stock object:
function formatStockNumbers(stock) {
var updateTuples = [
[ "price", formatPrice( stock.price ) ],
[ "change", formatChange( stock.change ) ]
];
return reduce( function formatter(stock,[propName,val]){
return setProp( propName, stock, val );
} )
( stock )
( updateTuples );
}
We create the updateTuples
array to hold both tuples (just arrays) with the property name and the new formatted value, for price
and change
respectively. We reduce(..)
(see Chapter 8) over that array, with the stock
object as the initialValue
. We destructure the tuple into propName
and val
, and then return the setProp(..)
call, which returns a new cloned object with the property setting applied.
Now let's define some more helpers:
var formatDecimal = unboundMethod( "toFixed" )( 2 );
var formatPrice = pipe( formatDecimal, formatCurrency );
var formatChange = pipe( formatDecimal, formatSign );
var processNewStock = pipe( addStockName, formatStockNumbers );
The formatDecimal(..)
function takes a number (like 2.1
) and calls its toFixed( 2 )
method call. We use Chapter 8's unboundMethod(..)
to create a standalone late-bound method.
formatPrice(..)
, formatChange(..)
, and processNewStock(..)
all use pipe(..)
to compose some operations left-to-right (see Chapter 4).
For creating our observables (see Chapter 10) from our event emitter, we're going to want a helper that's a curried (see Chapter 3) standalone of RxJS's Rx.Observable.fromEvent(..)
:
var makeObservableFromEvent = curry( Rx.Observable.fromEvent, 2 )( server );
This function is specified to listen to the server
(event emitter), and is just waiting for an event name string to produce its observable. We have all the pieces in place now to create observers for our two events, and to map-transform those observers to format the incoming data:
var observableMapperFns = [ processNewStock, formatStockNumbers ];
var [ newStocks, stockUpdates ] = pipe(
map( makeObservableFromEvent ),
curry( zip )( observableMapperFns ),
map( spreadArgs( transformObservable ) )
)
( [ "stock", "stock-update" ] );
We start with the array of event names (["stock","stock-update"]
), then map(..)
(see Chapter 8) that list to a list of two observables, and zip(..)
(see Chapter 8) that list to a list of observable-mapper functions; this mapping produces a list of tuples like [ observable, mapperFn ]
. Finally, we map(..)
that list of tuples with transformObservable(..)
, spreading out each tuple as individual arguments using spreadArgs(..)
(see Chapter 3).
The result is a list of transformed observables, which we array-destructure into the assignments for newStocks
and stockUpdates
, respectively.
That's it; that's our FP-Light approach to setting up our stock ticker event observables! We'll subscribe to these two observables in ch11-code/stock-ticker.js
.
Take a step back and reflect on our usage of FP principles here. Did it make sense? Can you see how we applied various concepts covered across the previous chapters from this book? Can you think of other ways to accomplish these tasks?
More importantly, how would you have done it imperatively, and how do you think those two approaches would have compared, broadly? Try that exercise. Write the equivalent using your well-established imperative approaches. If you're like me, the imperative form will still feel more natural.
What you need to get before moving on is that you can also understand and reason about the FP-style we just presented. Think about the shape (the inputs and output) of each function and piece. Do you see how they fit together?
Keep practicing until this stuff clicks for you.
If you felt pretty comfortable with the FP of the last section, you're ready to dig into ch11-code/stock-ticker.js
. It's considerably more involved, so we'll take our time to look at each piece in its entirety.
Let's start by defining some helpers that will assist in our DOM tasks:
function isTextNode(node) {
return node && node.nodeType == 3;
}
function getElemAttr(elem,prop) {
return elem.getAttribute( prop );
}
function setElemAttr(elem,prop,val) {
// !!SIDE EFFECTS!!
return elem.setAttribute( prop, val );
}
function matchingStockId(id) {
return function isStock(node){
return getStockId( node ) == id;
};
}
function isStockInfoChildElem(elem) {
return /\bstock-/i.test( getClassName( elem ) );
}
function appendDOMChild(parentNode,childNode) {
// !!SIDE EFFECTS!!
parentNode.appendChild( childNode );
return parentNode;
}
function setDOMContent(elem,html) {
// !!SIDE EFFECTS!!
elem.innerHTML = html;
return elem;
}
var createElement = document.createElement.bind( document );
var getElemAttrByName = curry( reverseArgs( getElemAttr ), 2 );
var getStockId = getElemAttrByName( "data-stock-id" );
var getClassName = getElemAttrByName( "class" );
These should be mostly self-explanatory. I used curry(reverseArgs( .. ))
(see Chapter 3) for getElemAttrByName(..)
instead of partialRight(..)
, only to squeeze slightly better performance in this particular case.
Notice that I called out the side effects of mutating a DOM element's state. We can't as easily clone a DOM object and replace it, so we settle here for some side effect of changing an existing one. At least if we have a bug in our DOM rendering, we can easily search for those code comments to narrow in on likely suspects.
matchingStockId(..)
shows one (of many!) usages of closure (see Chapter 2), by creating an inner function (isStock(..)
) that remembers the id
variable even though it'll be run later in a different scope.
Here's some other miscellaneous helpers:
function stripPrefix(prefixRegex) {
return function mapperFn(val) {
return val.replace( prefixRegex, "" );
};
}
function listify(listOrItem) {
if (!Array.isArray( listOrItem )) {
return [ listOrItem ];
}
return listOrItem;
}
Let's define a helper to help us get the child nodes of a DOM element:
var getDOMChildren = pipe(
listify,
flatMap(
pipe(
curry( prop )( "childNodes" ),
Array.from
)
)
);
First, we use listify(..)
to ensure we have a list of elements (even if it's only a single item in length). Recall flatMap(..)
from Chapter 8, which maps a list and then flattens a list-of-lists into a shallower list.
Our mapping function here maps from an element to its childNodes
list, which we make into a real array (instead of a live NodeList) with Array.from(..)
. These two functions are composed (via pipe(..)
) into a single mapper function, which is fusion (see Chapter 8).
Now, let's use this getDOMChildren(..)
helper to define utilities for retrieving specific DOM elements in our widget:
function getStockElem(tickerElem,stockId) {
return pipe(
getDOMChildren,
filterOut( isTextNode ),
filterIn( matchingStockId( stockId ) )
)
( tickerElem );
}
function getStockInfoChildElems(stockElem) {
return pipe(
getDOMChildren,
filterOut( isTextNode ),
filterIn( isStockInfoChildElem )
)
( stockElem );
}
getStockElem(..)
starts with the tickerElem
DOM element for our widget, retrieves its child elements, then filters to make sure we have the element matching the specified stock identifier. getStockInfoChildElems(..)
does almost the same thing, except it starts with a stock element, and narrows with different filters.
Both utilities filter out text nodes (since they don't work the same as real DOM nodes), and both utilities return an array of DOM elements, even if its just a single element.
We'll use a stockTickerUI
object to organize our three main UI manipulation methods, like this:
var stockTickerUI = {
updateStockElems(stockInfoChildElemList,data) {
// ..
},
updateStock(tickerElem,data) {
// ..
},
addStock(tickerElem,data) {
// ..
}
};
Let's first examine updateStock(..)
, as its the simplest of the three:
var stockTickerUI = {
// ..
updateStock(tickerElem,data) {
var getStockElemFromId = curry( getStockElem )( tickerElem );
var stockInfoChildElemList = pipe(
getStockElemFromId,
getStockInfoChildElems
)
( data.id );
return stockTickerUI.updateStockElems(
stockInfoChildElemList,
data
);
},
// ..
};
Currying the earlier helper getStockElem(..)
with tickerElem
gives us getStockElemFromId(..)
, which will receive data.id
. That <li>
element (actually, a list of that element) is passed to getStockInfoChildElems(..)
, giving us three child <span>
elements for the stock display info, which we call stockInfoChildElemList
. We pass this list and the stock data
message object along to stockTickerUI.updateStockElems(..)
for actually updating those <span>
s with the updated data.
Now let's look at stockTickerUI.updateStockElems(..)
:
var stockTickerUI = {
updateStockElems(stockInfoChildElemList,data) {
var getDataVal = curry( reverseArgs( prop ), 2 )( data );
var extractInfoChildElemVal = pipe(
getClassName,
stripPrefix( /\bstock-/i ),
getDataVal
);
var orderedDataVals =
map( extractInfoChildElemVal )( stockInfoChildElemList );
var elemsValsTuples =
filterOut( function updateValueMissing([infoChildElem,val]){
return val === undefined;
} )
( zip( stockInfoChildElemList, orderedDataVals ) );
// !!SIDE EFFECTS!!
compose( each, spreadArgs )
( setDOMContent )
( elemsValsTuples );
},
// ..
};
That's a bit to take in, I know. But we'll break it down statement by statement.
getDataVal(..)
is bound to the data
message object, having been curried after argument-reversing, so it's waiting for a property name to extract from data
.
Next, let's look at extractInfoChildElem
:
var extractInfoChildElemVal = pipe(
getClassName,
stripPrefix( /\bstock-/i ),
getDataVal
);
This function takes a DOM element, gets it DOM class, strips the "stock-"
prefix from it, then uses that value ("name"
, "price"
, or "change"
) to extract a value of that same name from data
via getDataVal(..)
. On the surface, that may seem like a slightly strange action.
The purpose, though, is to extract the values from data
in the same order as the <span>
elements (in stockInfoChildElemList
). We accomplish this by using extractInfoChildElem(..)
as the mapping function over that list, calling the resulting list orderedDataVals
.
Next, we're going to zip our list of <span>
s with this list of values, producing tuples:
zip( stockInfoChildElemList, orderedDataVals )
An interesting wrinkle that wasn't at all obvious up to this point is that because of how we defined the observables transforms, new-stock message objects will have a name
property in data
to match up with the <span class="stock-name">
element, but name
will be absent on stock-update message objects.
As a general notion, if the data message object doesn't have a property, we shouldn't update that corresponding DOM element. So, we need to filterOut(..)
any tuples where the value (in this case, in the second position) is undefined
:
var elemsValsTuples =
filterOut( function updateValueMissing([infoChildElem,val]){
return val === undefined;
} )
( zip( stockInfoChildElemList, orderedDataVals ) );
The result after this filtering is a list of tuples (like [ <span>, ".." ]
) ready for DOM content updating, which we assign to elemsValsTuples
.
Note: Since the updateValueMissing(..)
predicate is specified inline here, we're in control of its signature. Instead of using spreadArgs(..)
to adapt it to spread out a single array argument as two individual named parameters, we use parameter array-destructuring in the function declaration (function updateValueMissing([infoChildElem,val]){ ..
); see Chapter 2 for more information.
Finally, we need to update the DOM content of our <span>
elements:
// !!SIDE EFFECTS!!
compose( each, spreadArgs )( setDOMContent )
( elemsValsTuples );
We iterate this elemsValsTuples
list with each(..)
(see forEach(..)
discussion in Chapter 8).
Instead of using pipe(..)
as elsewhere, this composition uses compose(..)
(see Chapter 4) to pass setDomContent(..)
into spreadArgs(..)
, and then that is passed as the iterator-function to each(..)
. Each tuple is spread out as the arguments to setDOMContent(..)
, which then updates the DOM element accordingly.
That's two of the main UI methods down, one to go: addStock(..)
. Let's define it in its entirety, then we'll examine it step by step as before:
var stockTickerUI = {
// ..
addStock(tickerElem,data) {
var [stockElem, ...infoChildElems] = map(
createElement
)
( [ "li", "span", "span", "span" ] );
var attrValTuples = [
[ ["class","stock"], ["data-stock-id",data.id] ],
[ ["class","stock-name"] ],
[ ["class","stock-price"] ],
[ ["class","stock-change"] ]
];
var elemsAttrsTuples =
zip( [stockElem, ...infoChildElems], attrValTuples );
// !!SIDE EFFECTS!!
each( function setElemAttrs([elem,attrValTupleList]){
each(
spreadArgs( partial( setElemAttr, elem ) )
)
( attrValTupleList );
} )
( elemsAttrsTuples );
// !!SIDE EFFECTS!!
stockTickerUI.updateStockElems( infoChildElems, data );
reduce( appendDOMChild )( stockElem )( infoChildElems );
tickerElem.appendChild( stockElem );
}
};
This UI method needs to create the bare DOM structure for a new stock element, and then use stockTickerUI.updateStockElems(..)
to update its content as already described.
First:
var [stockElem, ...infoChildElems] = map(
createElement
)
( [ "li", "span", "span", "span" ] );
We create the parent <li>
and the three children <span>
elements, assigning them respectively to stockElem
and the infoChildElems
list.
To initialize these elements with the appropriate DOM attributes, we create a list of lists-of-tuples. Each item in the main list represents the four DOM elements, in order. Each tuple inside each sub-list represents an attribute-value pair to be set on the corresponding DOM element:
var attrValTuples = [
[ ["class","stock"], ["data-stock-id",data.id] ],
[ ["class","stock-name"] ],
[ ["class","stock-price"] ],
[ ["class","stock-change"] ]
];
We now want to zip(..)
a list of the four DOM elements with this attrValTuples
list:
var elemsAttrsTuples =
zip( [stockElem, ...infoChildElems], attrValTuples );
The structure of this list would look like:
[
[ <li>, [ ["class","stock"], ["data-stock-id",data.id] ] ],
[ <span>, [ ["class","stock-name"] ] ],
..
]
If we wanted to imperatively process this kind of data structure to assign the attribute-value tuples into each DOM element, we'd probably use nested for
-loops. Our FP approach will be similar, but with nested each(..)
iterations:
// !!SIDE EFFECTS!!
each( function setElemAttrs([elem,attrValTupleList]){
each(
spreadArgs( partial( setElemAttr, elem ) )
)
( attrValTupleList );
} )
( elemsAttrsTuples );
The outer each(..)
iterates the list of tuples, with each elem
and its associated attrValTupleList
spread out as named parameters to setElemAttrs(..)
via parameter array-destructuring as explained earlier.
Inside this outer iteration "loop", the sub-list of attribute-value tuples is iterated with an inner each(..)
. The inner iterator-function is an arguments-spread (of each attribute-value tuple) for the partial-application of setElemAttr(..)
with elem
as its first argument.
At this point, we have a list of <span>
elements, each filled out with attributes, but no innerHTML
content. We set the data
into the child <span>
elements with stockTickerUI.updateStockElems(..)
, the same as for a stock-update event.
Now, we need to append these <span>
s to the parent <li>
, and we do that with a reduce(..)
(see Chapter 8):
reduce( appendDOMChild )( stockElem )( infoChildElems );
Finally, a plain ol' DOM mutation side effect to append the new stock element to the widget's DOM:
tickerElem.appendChild( stockElem );
Phew! Did you follow all that? I recommend you go back and re-read that discussion a few times and practice with the code, before moving on.
Our last major task is to subscribe to the observables defined in ch11-code/stock-ticker-events.js
, attaching these subscriptions to the appropriate main UI methods (addStock(..)
and updateStock(..)
).
First, we notice that those methods each expect tickerElem
as first parameter. Let's make a list (stockTickerUIMethodsWithDOMContext
) that encapsulates the ticker widget's DOM element with each of these two methods, via partial application (aka, closure; see Chapter 2):
var ticker = document.getElementById( "stock-ticker" );
var stockTickerUIMethodsWithDOMContext = map(
curry( reverseArgs( partial ), 2 )( ticker )
)
( [ stockTickerUI.addStock, stockTickerUI.updateStock ] );
reverseArgs( partial )
is the same performance-optimized partialRight(..)
substitute from earlier. But this time, partial(..)
is the intended mapper function. To accomplish that, we need to curry(..)
it so we can pre-specify the second argument ticker
ahead of time; when each UI method is then mapped, it partially applies that function with ticker
. The two partially-applied functions in the resulting array are now suitable for observable subscription.
Though we're using closure to preserve the ticker
state with these two functions, in Chapter 7 we saw that we could have "kept" this ticker
value as a property on an object, perhaps via this
-binding each function to stockTickerUI
. Since this
is an implicit input (see Chapter 2) and that's generally not as preferable, I choose the closure form over the object form.
To subscribe to the observables, let's make an unbound-method helper:
var subscribeToObservable =
pipe( uncurry, spreadArgs )( unboundMethod( "subscribe" ) );
unboundMethod("subscribe")
is automatically curried, so we uncurry(..)
it (see Chapter 3), then adapt it with spreadArgs(..)
(again, see Chapter 3) so it'll spread out a single tuple array as its two arguments.
Now, we just need a list of the observables, so we can zip(..)
that with the list of context-encapsulated UI methods. That list of tuples can then each be subscribed with the subscribeToObservable(..)
helper we just defined in the previous snippet:
var stockTickerObservables = [ newStocks, stockUpdates ];
// !!SIDE EFFECTS!!
each( subscribeToObservable )
( zip( stockTickerUIMethodsWithDOMContext, stockTickerObservables ) );
Since we're technically mutating the state of those observables to subscribe to them, and moreover since we're using each(..)
-- pretty much always associated with side effects! -- we call that fact out with our code comment.
That's it! Spend the same time reviewing and comparing this code to its imperative alternatives as we did in with the stock ticker events discussion earlier. Really, take your time. I know it's been a long book, but your whole reading comes down to being able to digest and understand this kind of code.
How do you feel now about using FP in a balanced way in your JavaScript? Keep practicing just like we did here!
The example code we discussed in this chapter should be viewed in its entirety, not just in the broken out snippets as presented in this chapter. Stop right now and go read through the full files, if you haven't already. Make sure you understand them in full context.
This example code is not meant to be prescriptive of exactly how you should write your code. It's meant to be more descriptive of how to think about and begin approaching such tasks with FP-light techniques. It's meant to draw as many correlations between the different concepts of this book as possible. It's meant to explore FP in the context of more "real" code than we typically afford for a single snippet.
I am quite sure that as I learn FP better on my own journey, I will continue to improve how I would write this example code. What you see now is just a snapshot on my curve. I hope it will just be such for you, as well.
As we draw the main text of this book to a close, I want to remind you of this readability curve that I shared back in Chapter 1:
It's so important that you internalize the truth of that graph and set realistic expectations for yourself on this journey to learn and apply FP principles to your JavaScript. You've made it this far, and that's quite an accomplishment.
But don't stop when you dip towards that trough of despair and disenchantment. What's waiting on the other side is a way of thinking about and communicating with your code that's more readable, understandable, verifable, and ultimately, more reliable.
I can't think of any more noble goal for us as developers to strive towards. Thanks for sharing in my journey to learn FP principles in JavaScript. I hope your's is as rich and hopeful as mine!