- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
on 03-24-2019 06:56 PM
The goal of coding is to write code that is easily interpreted by those who read it, to find clearer and more articulate ways to convey a precise message. In this post, I’ll introduce piping and some of the effects that lead to enhanced readability...
Piping is the process of sequentially executing a list of functions so that the output of the previous function is used as the input of the next. This means an array of functions to the tune of
var leaversPipeline = [firstFunction, secondFunction, thirdFunction];
var outcome = pipe(leaversPipeline)(firstFunctionParameter);
Enhanced readability can come through:
- Concentrates fully on the high-level process
- function definition uniformity
-
- singular arity
- always return something
- less comments
- clean call-sites
- concise function body (smaller code surface)
- decreased complexity
- easier to name functions, thereby gaining more meaning
- abstractions
Concentration on high level process means that the first layer of abstraction is very basic, removing all possibly sugar, leaving for the reader simpler entry points that demonstrate what is happening without having to fully dissect code.
Function definition uniformity
To map outputs to inputs smoothly between function calls, single arity functions work best. For example, say 5 functions in the executing sequence have varying arity: 1, zero, 4, 2, and 3. Matching outputs to inputs would become daunting; couple that to figuring out how to use .apply or .call to match arguments to parameters, their order, and such, piping would turn into an adventure. What if they didn’t have a return value, there goes the pipe...
While functions can return complex objects, the result set is always one. Returning multiple items, say an object, then an array, then a primitive, isn’t possible as individual parameters are. So instead of trying to mimic outputs to varying arity functions, the obvious choice is to limit a parameters to one. Mapping output to input then becomes trivial. No need to worry about argument order, number of, etc. The concern is then, what does each function expect as to be ordered accordingly in the pipeline.
Cleaner call-sites
One parameter is simply less words than multiple ones. So calling a function is functionName(argument).
Concise function body
Coding for one parameter, rather then 2 or more takes less code.
Decreased complexity
The decrease in complexity is caused by managing the single function parameter. Code what needs to be done, then return the outcome. The need to have parameters interact with one another is gone. Parameter count directly affects the complexity of a function.
Easier to name functions and more meaningful
Functions limited in functionality become easier to name, thereby gaining more meaning. countCarWheels, postMessage. It’s just easier to come up with a name to something that does one and only one thing.
Reduced Comments
Because single parameter functions tend to be much easier to name meaningfully, function become self documenting, reducing the use of comments to functionality that requires some sort of cognitive reasoning beyond what is apparent with in the script.
As example
// find today’s leavers
var gr = new GlideRecord(‘sys_user’);
gr.addQuery(‘departure_date’, TODAY);
gr.query();
var leavers = [];
while( gr.next() ) {
//build list of leavers
leavers.push( gr.getUniqueValue());
}
//broadcast leavers
gs.eventQueue(‘boardcase.todays.leavers’,….);
Above is a common practice: comment, code, comment, code more. Yet, what needs to happen isn't quickly apparent; comments must be read from top to finish. That simple functionality still required 3 comments. But, if the comments can be replaced with a meaningful function call broadcastTodaysLeavers the entry point then abstracts everything away informing of what needs to happen:
broadcastTodaysLeavers(TODAY);
The one line documented itself, forgoing the need for three lines of comments embedded in multiple lines of code. Further break the code into logical groupings then, we got a nice sentence to read.
Piping and the practices it sort of entices one to follow lead to simpler and cleaner practices than those found in code with irregular function arity, or line by line coding how things should happen...
The goal is to allow the reader to more quickly interpret without reasoning what is happening at the top, the middle, the end, even more difficult, having to consider what might be happening outside the function. What happens to this param if that one is missing, etc. Hopefully rendering familiarity biases futile.
Example to Pipe or not to Pipe: Use Case
- Audit deactivate itil user by,
- reassign open tickets to reporting manager,
- revoking group membership only from groups managed by reporting manager
- notify the manager of the adjusted group membership, displaying in the email a list of current members and those removed.
No piping, and common to ServiceNow: How to programming style:
Some script include with some functionality
var ItilUserAuditorNoPipe = {
deactivationProcess: function deactivationProcess(deactivateUser) {
//get user manager
var manager = sysUser.getValue('manager');
//get assignee open tickets
var openTickets = new GlideRecord(task);
openTickets.addActiveQuery();
openTickets.addQuery('assigned_to', deactivateUser);
openTickets.query();
//reassign to manager
openTickets.setValue('assigned_to', manager);
openTickets.updateMultiple();
//remove from manager managed groups.
var managerMembersManaged = new GlideRecord('sys_user_grmember');
addQuery('group.manager', manager);
addQuery('user', deactivateUser);
managerMembersManaged.query();
managerMembersManaged.hasNext() && managerMembersManaged.deleteMultiple();
//notify of altered groups
gs.eventQueue('send.group.altered', manager, manager.getUniqueValue(), manager.getValue('email'));
}
}
With some Action script or another code to act on the queued event
var NotificationEmailScriptSendGroupAltered = function NotificationEmailScriptSendGroupAltered() {
//build object to notify of groups altered
var groupMembership = new GlideRecord('sys_user_grmember');
groupMembership.addQuery('group.manager', event.param2);
groupMembership.query();
var groupMembers = {};
var groupName;
var value;
//create object with group name as key and members as value array
while (groupMembership.next()) {
groupName = groupMembership.group.getDisplayValue();
value = groupMembership.user.getDisplayValue();
groupMembers[groupName] ?
groupMembers[groupName].push(value) :
(groupMembers[groupName] = [value]);
}
//print to template
var bodyContent = '<table>';
for (var group in groupMembers) {
var members;
bodyContent != '<tr><td>' + groupName + '</td><td><ul>';
members = groupMembers.reduce( function addMembers(content, member){
content.push('<li>'+member+'</li>');
return content;
}, []).join('');
bodyContent+= members + '</ul></tr>'
}
bodyContent+='<table>';
template.print( bodyContent);
}
The above two scripts are the normal SNow practice: Create a script include, place a function that carries out various functionality, then another somewhere else to respond. Placed the How to solve the use case inside a function, and line after line each step is carried out. This, while reasonable, leads to all sorts of maintenance overhead, be it testing, finding bugs, re-usability, readability. The code, while somewhat clean, requires too much interpretation. It behaves as if a library rather than a business process definition. There is just too much sugar just to know what is going on. I've done worse than that.
Piping, on the other hand, is like psuedocode. Present a high-level description of the use case as not to have to fully describe how to achieve the result. I like to think of it as this: I'm going to Knowledge by plane, rather than, describing the details of what happens... I'm waking up, brushing my teeth, calling uber, driving to air port, dismounting the vehicle, checking in, getting through security...
The details can instead be abstracted away to fulfill the use case to the tune of turning ItilUserAuditorNoPipe into:
var ItilUserAuditorPipe = {
deactivationProcess: function deactivationProcess(sysUser){
var pipeline = [
getAssigeeOpenTickets,
reassignUserTicketsToManager,
removeUserFromManagerManagedGroups,
notifyOfAlteredGroups
];
return pipe(pipeline)(sysUser);
}
};
For the sake of brevity, I won't refactor the above two scripts into a more logical grouping and summarize it.
Each one of the functions in the pipeline represent a step to take. The function name tells what process is happening, and the call site of the pipe() kicks it off.
The function array pipeline came from each step commented in the first script that was then turned it into functions. This way what is happening requires very little interpretation: deactivationProcess gets assignee opened tickets, reassigns them to the manager, removes user from groups, notify of altered groups. What happens is abstracted away.
The pipe function is:
var pipe = function pipe(pipeline) {
return function executePipe(initialParam) {
return pipeline.reduce(function callNextFuncWithPrev(inputArg, func) {
return func(inputArg);
}, initialParam);
};
};
The curried pipe function when called returns another function.
The return function takes one parameter initialParam that is used as the starting input for the first function in the pipeline's reduce call. Each time through reduce, intialParam becomes inputArg of the next function call through return func(inputArg).
The process of calling the next function in reduce with the returned inputArg as a parameter happens until each function in the pipeline is executed. The last value returned is that of the very last function.
Hopefully this is as helpful to some as I find it to be.
- 955 Views

- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
I like the proposal to use more functions than a long code piece. Functions make it much simpler to read (if named properly) - 100% agree!
Not to sure though on your piping part. I get how it works (having some linux shell background), but in this case... personally I would prefer proper functions with parameters. Just trying to remember my latest code pieces... I would probably struggle in most places to have only one parameter to be passed on - or would need to pass a JSON object :).
Giving up my parameters to allow piping would not make it easier to read I guess.
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
@fogg most pipe/compose helpers allow the first function in the sequence to accept multiple arguments
for example:
/**
*
* performs right to left composition
*/
function compose() {
var f = arguments[0];
var fns = Array.prototype.slice.call(arguments, 1);
var len = fns.length;
return function _run_() {
var i = 0;
var seed = f.apply(null, arguments);
while (len > i++)
seed = fns[i](seed);
return seed;
}
}
/**
*
* performs left to right composition
*/
function pipe() {
var fns = Array.prototype.slice.call(arguments, 0, -1);
var f = arguments[arguments.length - 1]
var len = fns.length;
return function _run_() {
var seed = f.apply(null, arguments);
var i = len;
while (i--)
seed = fns[i](seed);
return seed;
}
}
function add(a, b) {
return a + b;
}
function increment(i) {
return i + 1
}
function negate(i) {
return -i;
}
// combine left to right, first can take multiple args
var addPlus1 = pipe(add, increment, negate)
// combine right to left, first can take multiple args
var addPlus2 = compose(add, negate, add)
addPlus1(3, 4); // -((3 + 4) + 1)
addPlus2(3, 4); // (-(3 + 4)) + 1)
- Mark as Read
- Mark as New
- Bookmark
- Permalink
- Report Inappropriate Content
Indeed, arguments to functions can be complex objects, or, as mentioned already, the first function can take multiple arguments.
I used to do the multiple parameter approach until stumbling upon functional programming. Ever since my functions have become infinity simpler, allowing for much easier function composition, refactoring, unit testing, sharing functionality, etc. Now days my functions stay around 10 lines of code, mostly caused by limiting parameters to one. When needing more than one, a complex object does the trick.
The complex object is my preference when needing multiple parameters, say for setting values in an object. I use params as other functions can return a complex object that fits right into the next function, as opposed to having to manually destructure the result so that the parameters can be passed to another function. Say a func call extracts x values from a form to create an object. Rather than having one function do the extracting, validating, then creating the object, and saving it. I'd create one function to extract the values that returns a complex object, that is then handed to a validation function that returns the validate result, that then is handed to a create function. Those functions can be either piped or chained.
In a multi parameter scheme such operations would be handled differently, increasing verbosity, and likely complexity. How verbose, in a multi parameter function scheme, a universal function to create any one object, with varying fields, and validation strategies become? We'd have to imperitively handle the process, getting to the same point that declarative programming would in less lines.
I do not advocate for multi parameter functions but, certainly can't claim that it isn't helpful to others, nor that verbosity can mean very readable code. If it helps, that's what matters.