Javier Arroyo
Kilo Guru

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...

Comments
Fabian Kunzke
Kilo Sage
Kilo Sage

Hi Javier,

Really helpful article with an interesting topic. One thing that is on my mind: Is there a reason you are not extracting some of the functions? Example:

function onIffy (value) {
    return function fork (failure, success) {

        return value ?
            success(value) :
            failure(value);

    };

}

Instead of:

function onIffy (value, success, failure) {
    return fork (value, success, failure);

}

function fork (value, success, failure) {
    return value ?
        success(value) :
        failure(value);
}

That would make the first function irrelevant. I am sure, that i am missing something...

Or are you using the first function more like a constructor similar to a class? But then why not use a class instead?

Regards

Fabian

Javier Arroyo
Kilo Guru

Thank You, Fabian.


I'll fix onIffy to maybe make more sense of it. The previous version was a ServiceNowish way of showcasing the concept.

function onIffy (failureFn, successFn) {
   return function applyValue (value) {
      value 
         ? sucessFn
         : failureFn

   }
}

 

In regards to argument length; functional ideology tells us that unary functions build the most  elastic processes, inclusive of reduced complexity. It further says that two parameters are limited to apis that required it. Such as Array higher order functions (forEach, map, filter, etc). Or, when it's simply the most logical way to go because of the nature of the arguments, or the scope of the function (private within a class).

More than two parameters is not a desirable practice in functional programming because:

1. n-nary functions don't compose
2. It leads to excess redundancy each step of the way.
3. function complexity increases as arguments increase
4. can lead to duplication by necessity
5. mixture of concerns
6. clutters call sites
7. create ambiguity as to what each parameter should be
8. order must be remembered, etc

While n-nary and variadic functions are a common practice for classically trained programmers, in JavaScript its frowned upon. The loss of flexibility is too great to sacrifice.

The reason to adjust oniffy was to correct it to what it really ought to be so that it can become composable, and; n-nary argument convention states that the order of the parameters goes from the argument known first, to the one known last. So, onIffy knows that it will execute 2 functions up-front but, the very last thing it knows is value. That which it will execute the functions against. When both are known at the same time, take your pic, or allow the function name to dictate the order. This means that the name suggests what argument goes first. In the case of onIffy, failure goes first because the outcome of onfailure is known, as opposed to predicting what will happen to the value down the chain followed by onSuccess.

 

The reason that I did not use three parameters is because the function will not compose. Meaning that it will be impossible to use as a parameter to another function, nor to receive the output of another function, limiting its usability. It is also a curried function (a function that when executed returns another function) to allow for the creation of a context in which to execute value. I give it something, forget about it, then when needed, it knows what to do.

The three parameter approach requires the three parameters to be passed in each time, and passed in the correct order. There is no reason to pass them all, given that we already know what two algorithms are to be executed against every single value passed in. It is redundant to pass them. In addition, the curried function can be passed into Array Higher Order functions. The three parameter function will fail, leading to the creation of another function to adapt the parameters that a Higher Order function expects, to those required by our function. 

A class is not used because of the same issues as the three param function. A class doesn't compose. This means that if the predicate was put inside a class, it would need an adapter to talk to other code; and knowledge of the function that executes the final logic. To functional ethos, nothing good comes out of a class. 

Additionally, with your construct, the separation into two functions is redundant. Clean code, or imperative refactoring would call for the intermediary function to be eliminated to become:

function onIffy (value, success, failure) {
   return value 
      ? success(value)
      : failure(value);

}


Once that first refactoring was completed, it would further require that success and failure be grouped into a class of their own because they are logically bound to each other. But to do that, it is required that some sort of function be exposed to execute the functionality against value.

function Predicate (failure, success) {

    return {execute: execute};

    function execute (value) {
       return value
          ? success(value)
          : failure(value)
    }

}


I've cheated and not really used a constructor, or class, or SI (new Predicate) but, even here the ability to compose has been lost. The caller and receiver and programmer must know that execute needs to be called in order to Predicate(value).

Curried functions, outside the odd looking call site ( funcName(fail, success)(value) do make things easier. Because they already contain a context, they can be passed around all over the codebase without need to explicitly states so. The programmer can rest assure that no matter where his function goes, the outcome will be deterministic.

 

Lastly, because my pipe dream is to build these posts with the goal of introducing Functors and Monads without talking about what they are, nor how they work. The strategies used here will eventually make it to future posts so that readers understand why a solution works without having to reintroduce building blocks. 


That dream will demonstrate that all code can be pure and immutable aside a few functions (Monads or Functors) that mutate, or cause side effects. If that can be accomplished, there will be yet anther ServiceNow specific set of examples to increase the possibility of ServiceNow programmers to be seen in the same light as those in the cutting edge of programming.

My apologies if I attempted to explain the wrong questions.  Please let me know and I'll to better focus. 

Fabian Kunzke
Kilo Sage
Kilo Sage

Hi Javier,

Thanks for the explanation. I never thought about it this way as i am comping from an object oriented background and had so for zero to none contact with functional programming. I will see how i can integrate the use of both into my style as it looks super useful, but goes against what i am used from object calisthenics. Thanks for the in depth answer, as it really helps understanding the topic better when coming from a classes and functions point of view. To me functional developing has always looked like a novelty from the past, but you make me rethink that. Which is great! I will keep you updated on my progress.

Regards

Fabian

Version history
Last update:
‎08-23-2020 07:11 AM
Updated by: