Functional composition with compose and pipe in lodash/fp

Reading Time: 3 minutes

 134 total views

What is functional composition?

Functional composition is the technique of composing multiple functions into a complex function. In Mathematic definition,

f(x) = x + 1
g(x) = x * 2
h(x) = f(g(x)) = f(x * 2) = (x * 2) + 1

f(x) and g(x) are functions and h(x) is the composed function of f and g.

Functional composition is an efficient technique in processing list data and it is preferred over calling chained array methods to achieve the same result.

The code snippet below uses Array.filter, Array.map and Array.reduce to calculate the total, 105.

const multThree = (num: number) => num * 3
const addOne = (num: number) => num + 1
const isEven = (num: number) => num % 2 === 0
const combineSum = (acc: number, num: number) => acc + num

const data = [1,2,3,4,5,6,7,8,9,10]
const total = data.filter(isEven)
 .map(addOne)
 .map(multThree)
 .reduce(combineSum)

The chained methods produces intermediate arrays before we calculate the total.

[1,2,3,4,5,6,7,8,9,10].filter(number => number % 2 === 0) => [2,4,6,8,10]
[2, 4, 6, 8, 10].map(num => num + 1) => [3,5,7,9,11]
[3,5,7,9,11].map(num => num * 3) => [9,15,21,27,33]
[9,15,21,27,33].reduce((acc, num) => acc + num, 0) => 105

Array method chaining has the following drawback:

  • Produce the side effect of intermediate arrays and can lead to memory issue with big input array
  • Developers have to trace each step to visualize the input and output of it
  • Authors think in iterative approach and not functional approach

Functional composition can solve this problem because it combines functions to create a new function. When we feed list data to the new function, it manipulates the data once and combines the new result with the previous result.

In this post, we will see how lodash/fp offers compose, pipe and the counterpart of Array.methods to build a composed function. The composed function is capable of accepting input list and returning the final output by processing the list once.

let's go

Functional composition with compose

compose executes functions from right to left direction. If the functions are f(g(x)), we call compose(g(x), f(x)) in this manner.

Let’s rewrite the above function with high order function, compose.

import { compose, map, filter, reduce } from 'lodash/fp'

const composeFunction = compose(
    reduce(combineSum),
    map(multThree), 
    map(addOne), 
    filter(isEven)
)

const total2 = composeFunction(data)
console.log('with lodash/fp compose', total2)

First, we import compose, map, filter and reduce from ‘lodash/fp’. Since compose executes from right to left direction, reduce is the first argument of compose where as filter is the last one.

In the code, we use compose to combine filter, map and reduce to build a composed function called composeFunction.

When we feed data to composedFunction, composedFunction traverses the list once to calculate the total of 105.

composedFunction([1,2,3,4,5,6,7,8,9,10])
=> 9 + 15 + 21, 27, 33 
=> 105

The benefit of composedFunction is the removal of the the side effect of producing intermediate arrays.

Functional composition with pipe

Next, we rewrite composedFunction with pipe and called it pipeFunction

Similar to compose, pipe is a high order function that flows data through functions from left to right direction.

import { pipe, map, filter, reduce } from 'lodash/fp'

const pipeFunction = pipe(
    filter(isEven),
    map(addOne), 
    map(multThree), 
    reduce(combineSum),
)

const total4 = pipeFunction(data)
console.log('with lodash/fp pipe', total4)

The arguments of pipe are reversed but pipeFunction should achieve the same result as composedFunction.

pipeFunction([1,2,3,4,5,6,7,8,9,10])
=> 9 + 15 + 21, 27, 33 
=> 105

Finally, you can run the Stackblitz demo to play with the examples of compose, pipe and native methods of Array.

Final thoughts

Function composition has these benefits:

  • Remove the side effect of producing intermediate arrays
  • Developers do not have to trace the steps to derive the output list that becomes the input list of the next chained array method
  • Codes adopt functional approach and are point-free

In conclusion, I highly recommend developers to understand and practice functional programming in JavaScript and use the tool to replace array chaining to improve the efficiency of list processing.

Resources:

  1. Functiona light JS: https://github.com/getify/Functional-Light-JS/blob/master/manuscript/ch4.md/#chapter-4-composing-functions
  2. lodash/fp guide: https://github.com/lodash/lodash/wiki/FP-Guide
  3. ramdaJs: https://ramdajs.com/docs/#compose
  4. Stackblitz: https://stackblitz.com/edit/typescript-tmi9eg

Use Karma, Mocha, Chai and Coverage to run headless unit tests and generate lcov code coverage

Reading Time: 3 minutes

 166 total views,  2 views today

Github: https://github.com/railsstudent/image-gallery-native-js

1) Install gulp, mocha, chai, puppeteer, http-server as dev-dependencies

yarn add gulp mocha chai puppeteer http-server -D

2) Install all karma dependencies as dev-dependencies.

yarn add karma karma-chai karma-mocha karma-chrome-launcher mocha chai -D

3) Create test/bootstrap.karma.js file to share global variables among unit test cases.

'use strict';

const expect = chai.expect;
const assert = chai.assert;

4) Run karma command to generate karma.conf.js to specify Karma configuration.

./node_modules/.bin/karma init
// Karma configuration
// Generated on Sun Aug 26 2018 09:31:46 GMT+0800 (HKT)

process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    plugins: [
      "karma-chai",
      "karma-chrome-launcher",
      "karma-mocha",
      "karma-coverage"
    ],

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha', 'chai'],

    // list of files / patterns to load in the browser
    files: [
      'src/**/*.js',
      'test/bootstrap.karma.js',
      'test/**.test.js'
    ],

    // list of files / patterns to exclude
    exclude: [
      'test/gallery.test.js'
    ],

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'src/**/!(gallery).js': 'coverage'
    },

    // optionally, configure the reporter
    coverageReporter: {
      type : 'lcov',
      dir : 'coverage/',
      subdir: '.'
    },

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress', 'coverage'],

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['ChromeHeadless'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true,
  })
}

Explicitly list karma plugins for chai, browser launcher, mocha and code coverage.

plugins: [
   "karma-chai",
   "karma-chrome-launcher",
   "karma-mocha",
  "karma-coverage"
 ]

Include mocha and chai required by the unit test cases.

// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'chai'],

Load src/, test/bootstrap.karma.js and test/**/*.test.js in browser. Exclude test/gallery.test.js since this test file contains automated UI test cases

// list of files / patterns to load in the browser
files: [
  'src/**/*.js',
  'test/bootstrap.karma.js',
  'test/**.test.js'
],

// list of files / patterns to exclude
exclude: [
  'test/gallery.test.js'
]

Use Chromium instance of puppeteer to execute headless browser test

process.env.CHROME_BIN = require('puppeteer').executablePath();
browsers: ['ChromeHeadless'],

Add lcov code coverage. Code coverage results are kept in coverage/lcov-report directory

preprocessors: {
 'src/**/!(gallery).js': 'coverage'
},

// optionally, configure the reporter
coverageReporter: {
   type : 'lcov',
   dir : 'coverage/',
   subdir: '.'
}

Add reporters for headless browser testing and code coverage

reporters: ['progress', 'coverage']

5) Install gulp-karma to integrate karma with gulp

yarn add gulp-karma -D

Add gulp test task to run karma test runner

/**
 * Run test once and exit
 */
gulp.task('test', function (done) {
    new KarmaServer({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, done).start();
});

6) Set up npm script commands in package.json.

"scripts": {
   "test": "gulp test",
   "serve:coverage": "gulp test && http-server coverage/lcov-report/ -p 8001"
}

Test unit test cases in headless browser

yarn test

Serve lcov code coverage results at http://localhost:8001

yarn serve:coverage

7) Write mocha test case in test file.

'use strict';

describe('slideshow test', function() {
  let first, middle;
  let imageUrls = [];
  let slideshow = null;
  before(function() {
    first = 0;
    middle = 1;
  });

  describe('single image slideshow', () => {
    before(function() {
      // runs before all tests in this block
      imageUrls = [
        'image1.jpg'
      ];
      slideshow = new SlideShow(imageUrls);
      slideshow.setCurrentIndex(first);
    });

    it('slideshow has 1 image', function() {
      assert.strictEqual(slideshow.totalCount(), imageUrls.length);
    });

    it('current url should be the first url', function() {
      assert.strictEqual(slideshow.currentUrl(), 'image1.jpg');
    });

    it('initial index is always 0', function() {
      assert.strictEqual(slideshow.currentIndex(), first);
    });

    it('Slide show with 1 image is always the first image', function() {
      assert.strictEqual(slideshow.isFirstImage(), true);
    });

    it('Slide show with 1 image is always last image', function() {
      assert.strictEqual(slideshow.isLastImage(), true);
    });

    it('Show next image does nothing when there is 1 image', function() {
      assert.strictEqual(slideshow.showNext(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });

    it('Slide prev image with 1 image is always last image', function() {
      assert.strictEqual(slideshow.showPrev(), false);
      assert.strictEqual(slideshow.currentIndex(), first);
    });
  });
});

Finally, we are done.

Automate UI testing with Mocha and Puppeteer (Updated)

Reading Time: 2 minutes

 170 total views,  2 views today

Github: https://github.com/railsstudent/image-gallery-native-js

1) yarn add puppeteer mocha chai

yarn add puppeteer mocha chai

2) Create bootstrap.js file to share global variables among tests. Expose chai.expect, chai.assert and an instance of browser

'use strict';

const puppeteer = require('puppeteer');
const chai = require('chai');
const expect = chai.expect;
const globalVariables = { 
    browser: global.browser,
    expect: global.expect
};

// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};

// expose variables
before (async function () {
  global.expect = expect;
  global.browser = await puppeteer.launch(opts);
});

// close browser and reset global variables
after (function () {
  global.browser.close();

  global.browser = globalVariables.browser;
  global.expect = globalVariables.expect;
});

3) Puppeteer creates a headless browser that timeout after 50 seconds and slow down operation by 150 milliseconds.

// puppeteer options
const opts = {
  headless: true,
  slowMo: 150,
  timeout: 50000
};

4) Set up npm script commands in package.json to load the variables in bootstrap.js and execute all UI test cases in test/gallery.test.js directory

 "scripts": {
    "serve:test": "gulp build && http-server dist/ -p 8000",
    "puppeteer": "rm ./test/*.png; SCREEN_SHOT=false mocha test/bootstrap.js test/gallery.test.js",
    "puppeteer-screenshot": "rm ./test/*.png; SCREEN_SHOT=true mocha test/bootstrap.js test/gallery.test.js"
 }

5) Write mocha test case in test file.

describe('gallery test', function() {
  let page;
  this.timeout(TIMEOUT);

  before(async () => {
    try {
      page = await browser.newPage();
      await page.goto('http://localhost:8000/');
    } catch (e) {
      console.error(e);
    }
  });

  after(async () => {
    await page.close();
  });

 it('Shows first image src is not blank and has correct caption and visible buttons', 
     async () => {
    try {
      await page.waitForSelector('.image:nth-child(1)', { visible: true, timeout: 0 });
      await page.screenshot( {
        path: './test/test4.png'
      });

      const imageElement = await page.$('.image:nth-child(1)');
      await imageElement.click(); 
      await page.screenshot( {
        path: './test/test5.png'
      });
      
      const modalHandle = await page.$('.modal.show');
      const imageSrc = await page.evaluate((modal) => 
                modal.querySelector('#modal-image').src, modalHandle);
      const caption = await page.evaluate((modal) => 
                modal.querySelector('#caption').innerText, modalHandle);
      const [btnCloseDisplay, btnCloseOpacity,
        btnLeftDisplay, btnLeftOpacity,
        btnRightDisplay, btnRightOpacity
      ] = await page.evaluate((modal) => { 
        const { opacity: closeOpacity, display: closeDisplay } 
             = window.getComputedStyle(modal.querySelector('.close'));
        const { opacity: leftOpacity, display: leftDisplay } 
             = window.getComputedStyle(modal.querySelector('.left-arrow'));
        const { opacity: rightOpacity, display: rightDisplay } 
             = window.getComputedStyle(modal.querySelector('.right-arrow'));
        return [
          closeDisplay, closeOpacity,  
          leftDisplay, leftOpacity,
          rightDisplay, rightOpacity 
        ];
      }, modalHandle);

      expect(imageSrc).to.not.equal('');
      expect(caption).to.equal(`1 of ${NUM_IMAGES}`);
      expect(btnCloseDisplay).to.equal('inline-block');
      expect(btnRightDisplay).to.equal('inline-block');
      expect(btnLeftDisplay).to.be.equal('block');
      expect(btnCloseOpacity).to.equal('1');
      expect(btnRightOpacity).to.equal('1');
      expect(btnLeftOpacity).to.equal('0');

      const closeHandle = await page.$('.modal.show .close');
      await closeHandle.click();      
      closeHandle.dispose();
      modalHandle.dispose();
    } catch (e) {
      console.error(e);
      throw e;
    }
  });
});

6) Build development version and serve website at http://localhost:8000

yarn serve:test

7) Run UI test cases without creating screen shots

yarn puppeteer

8) Run UI test cases that creates screen shots

yarn puppeteer-screenshot

Finally, we are done.

Articles I want to read

Reading Time: < 1 minutes

 126 total views,  2 views today

Tensorflow

https://towardsdatascience.com/how-to-build-a-gesture-controlled-web-based-game-using-tensorflow-object-detection-api-587fb7e0f907

Ngrx

https://medium.com/@bo.vandersteene/advanced-pagination-with-ngrx-store-and-angular-5-f26ca4761cef
https://medium.com/@vlado.tesanovic/handling-keyboard-shortcuts-in-angular-with-redux-ngrx-c88907f17ca8

Angular
https://blog.angularindepth.com/angular-ivy-change-detection-execution-are-you-prepared-ab68d4231f2c
https://medium.com/frontend-coach/self-or-optional-host-the-visual-guide-to-angular-di-decorators-73fbbb5c8658
https://medium.com/@davidibl/advanced-reusable-custom-angular-validator-9ca5febef583
https://malcoded.com/posts/web-assembly-angular?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more

JS
https://blog.logrocket.com/using-trampolines-to-manage-large-recursive-loops-in-javascript-d8c9db095ae3

React Native
https://medium.freecodecamp.org/after-building-my-first-react-native-app-im-now-convinced-it-s-the-future-d3c5e74f8fa8

Week of August 23 to August 31, 2014

Reading Time: < 1 minutes

 164 total views

Coursera:
1) Programming Cloud Services for Android Handheld Systems: Submitted Quiz 6 and Assignment 2 in the past Sunday. The new assignment looked challenging at first after reading the README file and going through the source codes. Fortunately, code examples provided oAuth2 package that could directly use in the Gradle project. Furthermore, no effort to write CRUD methods to retrieve records from database because Spring Framework’s CRUDRespository class generates them automatically. My solution was create a Spring Controller to expose methods to receive HTTP requests from users. Then, the requests were delegated to CRUDRespository subclass to retrieve data and return it to users. Very simple!!!!

Programming:

2) Start my third AngularJS project that adds photo albums to store pictures taken in Open source Workshops and local meetup groups. This time, the project is bootstrap by Yeoman and Gulp because Matthew told me Gulp is better build system than Grunt. After using angular-gulp-generator to generate the skeleton project, I have to agree with his assessment.

Week of August 11 to August 22, 2014

Reading Time: < 1 minutes

 190 total views

Coursera:
1) Reproducible Research: Enroll Signature Track to earn my third Verified Certificate in Data Science Specialization.
2) Pattern-Oriented Software Architectures: Programming Mobile Services for Android Handheld Systems: Earn my second Verified Certificate in Mobile Cloud Computing with Android Specialization.
3) Advanced Competitive Strategy: Completed all 7 quizzes and open book final examination.
4) Programming Cloud Services for Android Handheld Systems: Submitted 5 quizzes and assignment 1. Assignment 2 was released but I cannot start until I watch all the videos of week 6. This course uses Spring framework to implement cloud services and instructor has covered Spring controller, Spring repository, Spring security, Spring data and Spring Rest.

Programming:

5) Deploy my second AngularJS size project to http://quiet-chamber-1998:herokuapp.com.
Features include:

  1. Show custom markers of pick-up locations on Google Map.
  2. Call Google Map Direction Service to render route.
  3. Allow user to input unlimited addresses to draw route to Hong Kong Science Park
  4. Define simple Restful API in NodeJS + Express Router
  5. Use Yeoman and Grunt to bootstrap AngularJS project.

Week of May 19 to May 31, 2014

Reading Time: < 1 minutes

 156 total views,  2 views today

Coursera:
1) Getting and Cleaning Data: Submitted Quiz 3 (15/15), Quiz 4 (15/15) and final course project.
2) Exploratory Data Analysis: Submitted Quiz 2 and Course Project 2.
3) Pattern-Oriented Software Architectures: Programming Mobile Services for Android Handheld Systems: Completed Quiz 1 to 3 and assignment 1 to 4. Unfortunately, wrong version of assignment 1 was submitted for peer-grading; therefore, I don’t expect to get good mark on it. Video lectures of week 4 are downloaded and will watch down when taking shuttle bus from Mei Foo to Hong Kong Science Park.

Programming:

4) Developing my first AngularJS project. Struggling to make multi-selection checkboxes working in my pet project. TODO item: make translation work at real time. When a button is clicked, labels change to either English or Traditional Chinese depending on user selection.

Week of May 5, 2014 to May 11, 2014

Reading Time: < 1 minutes

 120 total views,  2 views today

Coursera:
1) R Programming: Statement of Accomplishment with Distinction issued on May 9, 2014.
2) Getting and Cleaning Data: Watched week 1 and 2 video lectures. Submitted Quiz 1 (12/15) and Quiz 2 (15/15).
3) Exploratory Data Analysis: Watched week 1 video lectures. Submitted Quiz 1 (15/15) and Course Project 1.

Programming:

4) Developing my first AngularJS project. Using AngularJS, AngularJS UI and Bootstrap to display accessible facilities in Hong Kong.  http://localhost:8000/app/index.html