Introduction
JavaScript has several features that make it possible to use functional programming constructs. These constructs can be used to derive a function by building it up step by step. This will allow testing the parts of the function at each step in its development. This tip will demonstrate this with a simple task, it will create a function that takes an array of objects as input and appends the data in the objects to a table.
Using the Code
The demo web page has buttons for testing the code in this tip. Download the demo page and the FJS script file.
The goal is to create a function to append data from an array to a table. The function will be built out of basic JavaScript functions and some functional programming constructs that are also implemented in JavaScript. This process will allow the testing of each part of the function and then the parts will be combined together.
This project makes use of a library named FJS.js. This is an implementation of some basic functional programming constructs. Any functional programming library can be used instead.
First, a little functional programming:
Map(f,[a1, a2, ..]) = [f(a1), f(a2)]
Applies the function f to the elements of an array to create another array. Compose(f1,f2)(arg) = f1(f2(arg))
Creates a function out of the composition of the functions f1,f2
(can use more than two functions). LCurry(f,arg1)(arg2) = f(arg1,arg2)
Creates a function out of f
and the argument arg1
. When this new function is invoked, it will be that same as f
with arg1
and arg2
. Prop(propertyname)(object) = object.propertyname
A function that returns the property value of an object. Onto([f1,f2,...])(arg) = [f1(arg), f2(arg), ...]
Applies an array of functions [f1,f1,...]
to an argument to create an array. Foldl(f,initial,[arg1,arg2,...]) = f(f(initial,arg1),arg2)
Applies a function to an initial argument and the first element of an array and then recursively to the rest of the array.
The plan is to create a function in three phases:
- Create a function that returns an array of
td
elements from the fields in an element of the input data array. - Then create a function that returns a
tr
element that contains the td
elements from 1) - Lastly, create a function that appends the
tr
element from 2
) to the table and apply that function to all of the elements of the data array.
Building the Function
Define some aliases for the functional programming we will need.
var LCurry = FJS.LCurry;
var Prop = FJS.Prop;
var Compose = FJS.Compose;
var OnTo = FJS.OnTo;
var Foldl = FJS.Foldl;
var Map = FJS.Map;
The data array that will be the input is:
var A = [{title:"On the Revolutions of Heavenly Spheres",author:"Nicolaus Copernicus"},
{title:"Dialogues Concerning the Two Sciences",author:"Galileo Galilei"},
{title:"The Geometry",author:"Rene Descartes"}];
Select an index for testing, this will later be removed.
var i = 2;
Define some basic support functions:
function AppendTo(Parent,Child)
{
Parent.appendChild(Child);
return Parent;
}
function TDNode(innerHTML)
{
var tdele = document.createElement("td");
tdele.innerHTML = innerHTML;
return tdele;
}
function TRNode(TREle,TDEle)
{
if (TREle === null)
TREle = document.createElement("tr");
TREle.appendChild(TDEle);
return TREle;
}
Define some variables that will hold the final functions.
var Cells;
var TR;
var TableAppend;
Now, derive the program in a step by step fashion so that each step can be tested. Each step will be assigned to a variable that begins with the word step. In this way, a breakpoint can be set on these lines to check the output at that stage of the program. Later, these lines can be deleted.
For 1)
var TargetTable = document.getElementById("TargetTable1");
Create a td
element with the TDNode
and Prop
functions in the usual JavaScript way. This is then written in the form F(A[i]) with the Compose
function.
var step1 = TDNode(Prop("title")(A[i]));
var step2 = Compose(TDNode,Prop("title"))(A[i]);
Do a similar thing to create functions for the other fields in the object. Put these together in an array and apply the array of functions to A[i]
with Onto
. This will create an array of td
elements. Then, use the LCurry
to rewrite the Onto
in the form F(A[i])
. This is then the Cells
function.
var step3 = OnTo([Compose(TDNode,Prop("title")), Compose(TDNode,Prop("author"))],A[i]);
var step4 = LCurry(OnTo,[Compose(TDNode,Prop("title")), Compose(TDNode,Prop("author"))])(A[i]);
Cells = LCurry(OnTo,[Compose(TDNode,Prop("title")), Compose(TDNode,Prop("author"))]);
For 2), want to derive the TR
function that creates a tr
element and appends to it the array from Cells
. This is done by recursively applying the TRNode
function (defined previously) to the array returned by Cells
, which is then rewritten with LCurry
to "remove" the TRNode
function and make a composition. Then, use Compose
to get the desired function.
var step5 = Foldl(TRNode,null,Cells(A[i]));
var step6 = LCurry(Foldl,TRNode,null)(Cells(A[i]));
var step7 = Compose(LCurry(Foldl,TRNode,null),Cells)(A[i]);
TR = Compose(LCurry(Foldl,TRNode,null),Cells);
Finally for 3), use the AppendTo
function to append the row element from TR
to the table. Then use LCurry
to make the AppendTo
a function. This is then a composition of the LCurry
and TR
so using Compose
converts the function into the form F(A[i])
. Apply this function to each element of A
with Map
. And again, LCurry
puts the equation in the desired format. So the final product, TableAppend
, is:
var step8 = AppendTo(TargetTable,TR(A[i]));
var step9 = LCurry(AppendTo,TargetTable)(TR(A[i]));
var step10 = Compose(LCurry(AppendTo,TargetTable),TR)(A[i]);
var step11 = Map(Compose(LCurry(AppendTo,TargetTable),TR),A);
var step12 = LCurry(Map,Compose(LCurry(AppendTo,TargetTable),TR))(A);
TableAppend = LCurry(Map,Compose(LCurry(AppendTo,TargetTable),TR));
Removing all of the step lines gives the final product:
Cells = LCurry(OnTo,[Compose(TDNode,Prop("title")), Compose(TDNode,Prop("author"))]);
TR = Compose(LCurry(Foldl,TRNode,null),Cells);
TableAppend = LCurry(Map,Compose(LCurry(AppendTo,TargetTable),TR));
One nice feature of functional programming is that TableAppend
can be written as a single function by substituting for TR
, and Cells
.
TableAppend = LCurry(Map,Compose(LCurry(AppendTo,TargetTable),
Compose(LCurry(Foldl,TRNode,null),LCurry(OnTo,[Compose(TDNode,Prop("title")),
Compose(TDNode,Prop("author"))]))));
This is pretty complicated, keeping it in three previous functions makes it easier to read.
To Add Attributes to the Table
Now, we will add attribute objects to the function. These attributes will be applied to the td
elements that are created from the A
array. This will only require a change in the Cells
function.
First, change the TDNode
function to get an attribute object parameter and apply the properties of the object as attributes of the TD
node.
function TDNode2(AttrObject,innerHTML)
{
var tdele = document.createElement("td");
tdele.innerHTML = innerHTML;
var p;
for (p in AttrObject) {
tdele.setAttribute(p,AttrObject[p]);
}
return tdele;
}
Now, create the attribute objects for title and author. These objects just use a style
attribute but any attributes can be put into the objects.
var AttrTitle = {style:"background-color:LightBlue"};
var AttrAuthor = {style:"background-color:LightGreen"};
Then, rewrite the previous functions as follows: use LCurry
and then Compose
to form the function that creates the td
node with the attributes
object.
var step13 = TDNode2(AttrTitle,Prop("title")(A[i]));
var step14 = LCurry(TDNode2,AttrTitle)(Prop("title")(A[i]));
var step15 = Compose(LCurry(TDNode2,AttrTitle),Prop("title"))(A[i]);
This is then inserted into the array in the definition of Cells
to get:
var step16 = LCurry(OnTo,[Compose(LCurry(TDNode2,AttrTitle),Prop("title")),
Compose(LCurry(TDNode2,AttrAuthor),Prop("author"))])(A[i]);
Cells = LCurry(OnTo,[Compose(LCurry(TDNode2,AttrTitle),Prop("title")),
Compose(LCurry(TDNode2,AttrAuthor),Prop("author"))]);
TR
and TableAppend
stay the same.