Function composition on data pipelines and error handling.

We create a higher-order function that returns a new function while taking two series of arguments, during its first and second invocations.

//The arguments values are cached in the returned function
function translate2d(dx, dy) {

  return function (x, y){
    return [dx + x, dy + y]  
  }
}

const move2x = translate2d(2, 0);    //move2x() is the returned function 
const result = move2x(4, 8);         //result is the returned operation result
console.log( result )                //[6, 8]

This is called function composition, a process that combines functions to break down complex operations into smaller, more manageable pieces.

//Local variables can be set and used down the line
//Passed functions can be used with later invoked arguments
function memoize(f) {
  let preX, preY, preR
  
  return function (x, y) {
    if (preX === x && preY === y) {
      console.log("already casted")
      return preR
    }
    preX = x
    preY = y
    return preR = f(x, y)
  }
}

The returning functions can be invoked with new arguments, allowing for higher-order functions to be more modular across different invocations.

//We set the argument function being used in the composition
//and its argument on the second invokation
function addition(x,y){
  return [x,y]
}

const memo1 = memoize( adding );      
memo1(1, 5)  //f(x) == [5, 5]
memo1(5, 5)  //[5,5]
memo1(5, 5)  //[5,5] "already casted"
Spread operator arguments and function composition order

We can use the spread operator on the array returned by a function, which allows us to use the destructured array elements as arguments of the composed function.

Both functions accept the same type of arguments and can perform different operations when their order is changed.

//The g(x, y) first needs to deconstruct the returned [x, y]
function uni(x,y){
  return [x+3, y+1]
}

function dui(x,y){
  return [x*2, y*3]
}

function composeTransform(f, g) {
  return function (x, y) {
    return g(...f(x, y))
  }
}

let tent = composeTransform( uni, dui)    //[[2+3]*2 , [1+1]*3] 
tent(2,1)                                 //[10,6] 

let tent1 = composeTransform( dui, uni )  //[[2*2]+3, [1*3]+1 ] 
tent1(2, 1)                               //[7, 4]

A function composition determines its evaluation order, based on its function (nesting), with the innermost function being applied first.

The argument passed to the composed function is used by its first function, and then each successive function receives the previous output as its argument, enabling pipeline data processing.

//sumNumbers for last because it doesn't return an array like the others
let filterOddNumbers = (arr) => arr.filter( x => x % 2 !== 0 )
let squareNumbers = (arr) => arr.map( x => x * x )
let addOneToEach = (arr) => arr.map( x => x + 1 )
let sumNumbers = (arr) => arr.reduce((acc, x) => acc + x, 0)

let duo = (f, g) => (array) => g( f(array))
let trio = (f, g, h) => (array1) => h( g( f( array1 )))

const pipeline = duo( 
  trio( filterOddNumbers, squareNumbers, addOneToEach ), 
  sumNumbers
)
//No even numbers, square each, + 1 each ==> sum
let data = [1, 2, 3, 4, 5, 6, 7, 8, 9]

const result = pipeline(data);  //[1, 3, 5, 7, 9], Math.pow(2), x+1, arr.reduce()
console.log(result);    // Output: 170

Functions execution order on reduce() function compositions.

The reduce() method accumulates values from an array, iterating between its elements.

//initial value (v) + the rest (f) from left/right
console.log( [1, 2, 3].reduce((v, f) => v + f ) ) //6

We can combine multiple functions into a single variadic function composition, by destructuring their arguments as an array. The provided argument serves as the initial input, and each function's output becomes the input for the next one in the array.

//x is extracted from each single function, x == v 
let compose1 = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

//Each x is the returned f(x) of the previous array function ((x*2)+1)*x)
let double = x => x * 2;
let addOne = x => x + 1;
let square = x => x * x;

//We provide the x argument on the function composition invocation.
let doubleThenAddOneThenSquare = compose1(square, addOne, double);
console.log(doubleThenAddOneThenSquare(5)); // Output: 121 //52 if reverse order 

In this function composition, each destructed function returns a new function rather than a value, returning a new function composition. The order of execution in the returned function composition will be inverted, from right-left (as in reduceRight) to top-bottom. Each function has access to the function input from the previous function and the arguments passed when the composition is invoked.

We compose reference functions, that will trigger once they start being reduced.

//Deconstructs functions from right to left
let compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

//single deconstructed function made by 2 functions
let secondo = (fn) => (...args) => {
  console.log("secondo?")
  return fn(...args) + 1
}

let primo = (fn) => (...args) => {
  console.log("primo?")
  return fn(...args) + 1   
}

let final = (massa) => massa + 5

//Composed reference function being returned primo(secondo((final)))
let ricomposed = compose(
  primo,
  secondo
)(final)

let resulted = ricomposed(5)
console.log( resulted )  //primo?, secondo?, 7
Returned function composition execution order

The reduceRight() method composes functions from right to left, on an [primo, secondo] array, but the function composition returns the primo() function first.

The secondo function is triggered first, but it returns a function (...args), which is then called on the second function composition call.

//We add a console.log() to the first invocation on reduceRight()
function primo1(){
  return function (fn){
    console.log( "Actual Primo" )

    return function (...args){
      console.log( "Primo?" )
      return fn(...args) + 1
    }
  }
}

function secondo2(){
  return function (fn){
    console.log( "Actual Secondo" )

    return function (...args){
      console.log( "Secondo?" )
      return fn(...args) + 1
    }
  }
}

let final = (massa) => massa + 5

let compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

let ricomposed = compose(
  primo1(),
  secondo2()
)(final)
//The reduceRight() is being respected
//"Actual Primo", "Actual Secondo"
//"Secondo?", "Primo?", 12

The function composition being returned from the reduceRight() is.

//primo(secondo(final)) is equivalent to
function ricomposed() {
  return function() {
    console.log("Primo?")
    return function(massa) {
      console.log("Secondo?");
      return final(massa) + 1 + 1;
    };
  };
}

console.log( ricomposed()()(5) )  //"Primo?", "secondo?", 7

Function composition on Error Handling

A function composition can include multiple error-handling blocks, each deconstructed function try/catch a specific error instance, and returns a safe output from the argument function.

//Error instances
class ValidationError extends Error {}
class NotFound extends Error {}

let compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

//Argument function 
let viewHike = (message) => {
  if (message === 'view hike mirror lake') {
    return 'Details about <mirror lake>';
  } else if (message === 'view hike lost lake') {
    throw new NotFound();
  } else {
    throw new ValidationError();
  }
};
//Error instance filter a
let rescue = (error, type) => {
  if (!(error instanceof type)) {
    throw error;
  }
};
//Decostructed try/catch function 
let swallow = (type) => (fail) => (fn) => (...args) => {
  try {
    return fn(...args);
  } catch (error) {
    rescue(error, type);
    return fail(error);
  }
}
//Composed function with (argument) function
let safeViewHike = compose(  
  swallow(ValidationError)(() => 'Invalid format.'),
  swallow(NotFound)(() => 'No such hike.'),
)(viewHike);

let chatbot = safeViewHike;

chatbot('view hike mirror lake') )  // => 'Details about <mirror lake>'
chatbot('view hike lost lake'));   // => Error 'No such hike.'
chatbot('show hike blue ridge')); // => 'Invalid format.'

The created error instances are used in the argument function, any thrown error will be handled by the next decostructed try/catch functions.

class ValidationError extends Error {}
class NotFound extends Error {}

let viewHike = (message) => {
  if (message === 'view hike mirror lake') {
    return 'Details about <mirror lake>';
  } else if (message === 'view hike lost lake') {
    throw new NotFound();
  } else {
    throw new ValidationError();
  }
};

Each destructed function includes 2 arguments, the (type) error instance and the (fail) returned string. They then include the argument function (fn) and the composed function argument (...args).

If an error is thrown by the argument function, it will be passed to the rescue(), and, if no further error is thrown, no other try/catch function will be called.

//The return being triggered closes the function
let safeViewHike = compose(  
  swallow(ValidationError)(() => 'Invalid format.'),
  swallow(NotFound)(() => 'No such hike.'),
)(viewHike);

let swallow = (type) => (fail) => (fn) => (...args) => {
  try {
    return fn(...args);
  } catch (error) {
    rescue(error, type);
    return fail(error);
  }
}

The rescue function compares the returned error with the error instance from the destructed function, if the type doesn't match then it throws an error for the next try/catch function.

let rescue = (error, type) => {
  if (!(error instanceof type)) {
    throw error;
  }
};

A try/catch function doesn't follow the standard top-bottom execution order. Instead, it skips to the first available function, and propagates any thrown errors to the outer try/catch blocks.

//A decomposed example of the previous function
let complete = (...args) => {
  try {
    try {
      return viewHike(...args);
    } catch (error) {
      if (error instanceof NotFound) {
        return 'No such hike.';
      } else {
        throw error;
      }
    }
  } catch (error) {
    if (error instanceof ValidationError) {
      return 'Invalid format.';
    } else {
      throw error;
    }
  }
}

complete('view hike mirror lake') // => 'Details about <mirror lake>'

Last updated

Was this helpful?