Repurposing Conditional Statements (if/else)
In a previous article titled Checking truthy/falsy alternate approaches to conditionally check truthy/falsy values was introduced. This post will expand on the statement made in Checking truthy/falsy where it was suggested that a truthy/falsey conditional can be made more robust and repurposed as a general predicate checker.
To limit the size of this port, I will not be building functionality for both variations of Iffy checker functions. Only the curried version (below) will be addressed:
function onIffy (value) {
return function fork (failure, success) {
return value ?
success(value) :
failure(value);
};
}
onIffy is a curried function of depth 2. The first function call onIffy(someValue) returns a function that accepts two functions as parameters. The parameters stand for the functions to be executing when the ternary operation fails (1st/left param) or succeeds (2nd/right param) onIffy(someValue)(failureFn,successFn).
return value ?
success(value) :
failure(value);
While the concept is pretty potent, the strategy used to determine the first operand of the ternary function is so limiting that it leads to contentious rebuilding of the very same logic just to address other predicates. Below will be an attempt at generalizing and scaling onIffy
The Gist of the Solution
Switch the first operand of the ternary into a function call.
Such shift allows the truthy/falsy utility function to become a general purpose predicate (something that is true or false) function that can be shared; isolating the use of "boilerplate" if/else to a single location while determining what should happen at runtime.
Developers can then fully focus on what should happen when a condition (if/else) succeeds vs fail rather than having to also write another if/else to branch logic.
The Revised onIffy truthy/falsy function
function Predicate (predicateFn) {
return function predicateStrategy (isFailure, isSuccess) {
return function applyPredicateStrategy (value) {
return predicateFn(value) ?
isSuccess(value) :
isFailure(value);
};
};
}
The revised version goes from a depth-2 to a depth-3 curried function that is called quite foreign to orthodox JavaScript. It also gets a new name Predicate.
Usage
Predicate(aPredicateStrategy)(aFailureFunction,aSuccessFunction)(valueToCheck).
Looking past its odd looking call point two key additions were made:
1:An additional function wrapping the original functionality that accepts a predicate function as a parameter. This predicateFn parameter contains the strategy that determines what it means to be true or false.
The wrapper:
function Predicate (predicateFn) {
return ...
}
2: the first operand of the ternary operator has been adjusted from a valued param to a function call. Because predicateFn parameter is now a function, it can be called against the value. The execution result of that function determines failure and success. The hard binding of truthy/falsy is gone.
predicateFn(value) ?
isSuccess(value) :
isFailure(value);
To see how it works, there will be 3 predicate strategies created and two for GlideRecord. This will be done to showcase Predicate as reusable.
Reusable Predicate (true/false) Strategies:
function identity (value) {//truthy check
return value;
}
function isNothing (value) {
return value === undefined || value === null;
}
function isString (value) {
return typeof value === 'string';
}
Identity() in programming means a neutral value function that given a parameter, it returns it unchanged. It basically reveals value as being itself. In terms of truthy/falsy, when used in a conditional evaluation, it determines those items that either truthy/falsy.
isNothing() is a function that gives meaning to what it is to be nothing: undefined or null
isString() identifies a value as being of type string
Unit Tests for Idenity, isNothing, isString functions
var values = [1, true, false, '', [], {}, 0, NaN, null, undefined];
gs.debug('Start truthy check');
var truthiesFn = Predicate(identity)(failure, success);
values.map(truthiesFn);
gs.debug('End truthy check\n\n');
gs.debug('Start isNothing check');
var nothings = Predicate(isNothing)(failure, success);
values.map(nothings);
gs.debug('End isNothing check\n\n');
gs.debug('Start isString check');
var isStrings = Predicate(isString)(failure, success);
values.map(isStrings);
gs.debug('End isString check\n\n');
Results
*** Script: [DEBUG] Start truthy check
*** Script: successful -> (1)
*** Script: successful -> (true)
*** Script: failure -> (false)
*** Script: failure -> ()
*** Script: failure -> ()
*** Script: successful -> ([object Object])
*** Script: failure -> (0)
*** Script: failure -> (NaN)
*** Script: failure -> (null)
*** Script: failure -> (undefined)
*** Script: [DEBUG] End truthy check
*** Script: [DEBUG] Start isNothing check
*** Script: failure -> (1)
*** Script: failure -> (true)
*** Script: failure -> (false)
*** Script: failure -> ()
*** Script: failure -> ()
*** Script: failure -> ([object Object])
*** Script: failure -> (0)
*** Script: failure -> (NaN)
*** Script: successful -> (null)
*** Script: successful -> (undefined)
*** Script: [DEBUG] End isNothing check
*** Script: [DEBUG] Start isString check
*** Script: failure -> (1)
*** Script: failure -> (true)
*** Script: failure -> (false)
*** Script: successful -> ()
*** Script: failure -> ()
*** Script: failure -> ([object Object])
*** Script: failure -> (0)
*** Script: failure -> (NaN)
*** Script: failure -> (null)
*** Script: failure -> (undefined)
*** Script: [DEBUG] End isString check
Reusable Predicate (true/false) Strategies for GlideRecords:
function isValidRecord (gr) {
return gr.isValidRecord();
}
function isValidField (gr) {
return function inGR (fieldName) {
return gr.isValidField(fieldName);
};
}
isValidRecord and isValidField are wrappers to GlideRecord.isValueRecord/Field to allow passing them around as parameters to other functions.
Testing with GlideRecords
The utility function below will be used to "convert" extracted field values from GlideRecord into an Object Literal.
function getFieldValues (fieldNames) {
function addValueToCollector (collector) {
return function added (fieldName) {
collector[fieldName] = gr.getValue(fieldName);
return collector;
};
}
function getFromRecord (gr) {
return function asObjectLiteral (collector, fieldName) {
Predicate(isValidField(gr))(failure, addValueToCollector (collector))(fieldName);
return collector;
};
}
return function getValue (gr) {
return fieldNames.reduce(getFromRecord(gr), {});
};
}
When initially executed, the depth-2 curried getFieldValues function accepts an array of GlideRecord.fieldName[s]. It returns another function getValue that accepts a GlideRecord as a parameter, as well as creating two functions to pass around to Array.reduce
The curried function is used to create a context which supply the revised Predicate function.
addValueToCollector is also a 2 deep curried function. The first call accepts an Object Literal as parameter in which to collect GlideRecord fields and their values, returning the added function. added does the actual field value extraction. Notice that in practice, such strategy will be the only place where getting GlideRecord field values will appear in the entire code base. This approach can also be altered to accept a field value extraction strategy as not to hard code GlideRecord.getValue(fieldName).
getFromRecord too is a 2 deep curried function that accepts a GlideRecord as a parameter, returning the function asObjectLiteral. asObjectLiteral is meant to work as Array.reducer function. It's two parameters are collector (object literal) and the fieldName (placeholder for the current item from the Array). This is where the revised Predicate function will be called using the isValidField strategy.
When Predicate.isValid is successful, it executes addValueToCollector; if unssucessful it gs.debug. The outcome will be an Key/Value Pair object.
Contrived Unit Test with GlideRecord
gs.debug('Start isValidRecord check');
var extractFieldValuesFn = getFieldValues(['number', 'short_description', 'dog', 'cat', 'pet', 'sys_created_on']);
var isValidRecordFn = Predicate(isValidRecord)(failure, extractFieldValuesFn);
var gr = new GlideRecord('change_request')
gr.setLimit(3);
gr.query();
while( gr.next() ) {
gs.debug('what the ' + JSON.stringify(isValidRecordFn(gr)));
}
gs.debug('End isValidRecord check\n\n');
The worthy items are spotlighted by:
1. extractFieldValuesFn - identifies behavior used to retrieve values from GR, Testing for valid/invalid fields
2. isValidRecordFn - called within while (gr.next()) . When GlideRecord.isValid = true, extractFieldValuesFn is executed; otherwise, failure is called.
Result
*** Script: [DEBUG] Start isValidRecord check
*** Script: failure -> (dog)
*** Script: failure -> (cat)
*** Script: failure -> (pet)
*** Script: [DEBUG] what the {"number":"CHG0001046","short_description":"DLP alert","sys_created_on":"2020-01-10 03:05:02"}
*** Script: failure -> (dog)
*** Script: failure -> (cat)
*** Script: failure -> (pet)
*** Script: [DEBUG] what the {"number":"CHG0001009","short_description":"Admin Privileges Abused","sys_created_on":"2019-11-13 08:34:11"}
*** Script: failure -> (dog)
*** Script: failure -> (cat)
*** Script: failure -> (pet)
*** Script: [DEBUG] what the {"number":"CHG0001028","short_description":"Person in accounting installed an Excel add-on","sys_created_on":"2019-11-24 01:42:45"}
*** Script: [DEBUG] End isValidRecord check
Summary
By creating a predicate utility function if/else logic can be isolated into a single place, allowing the author to code what is meant by true/false, and what strategy to execute given the evaluation. Approaches like this tend to isolate code logically, making it easier to build libraries that when composed create a tightly neat code base limited to what is happening without added noise.
The code in this post is fairly similar to utility functions used in our production environment. Single purpose functions are built and passed around from function to function, reducing noise an isolating behavior.
The Contrived GlideRecord.isValidRecord test for us would be:
ChangeRequest()
.query(ChangeRequestQuery.RANDOM_THREE)
.composeWhile(isValidRecordFn(gr))
Happy SNowing...
Labels:
https://www.servicenow.com/community/developer-articles/repurposing-conditional-statements-if-else/ta-p/2321416
