ES6 Functional Programming Guide

website·Indexed 31 days ago

https://yanxuewen.cn/ES6_Functional_Programming

A guide to functional programming concepts in ES6, covering higher-order functions, currying, function composition, functors (including MayBe and Either), and functional array methods like map and filter.

Tokens
4.1K
Snippets
23
Records
24
Agent score
57%

What's inside ES6 Functional Programming

  1. Understand Higher-Order Functions (HOF) basics

    A Higher-Order Function is a function that either takes one or more functions as arguments or returns a function as its result. They are primarily used to abstract logic and solve general problems by separating the iteration or control flow from the specific operation being performed.
  2. Flatten nested MayBe containers using join and chain

    When a map function returns another MayBe container, you end up with nested containers (e.g., MayBe { value: MayBe { value: 5 } }).

    • join(): Flattens the nested container by one level.
    • chain(): A combination of map and join, allowing you to transform a value into a new container without increasing the nesting level.
    MayBe.prototype.join = function () {
      return this.isNothing() ? MayBe.of(null) : this.value;
    };
    
    MayBe.prototype.chain = function (f) {
      return this.map(f).join();
    };
    
    // Usage
    const nested = MayBe.of(MayBe.of(5));
    console.log(nested.join().value); // 5
    
    const result = MayBe.of(5).chain(val => MayBe.of(val + 1));
    console.log(result.value); // 6
  3. Use the MayBe Functor to handle null or undefined values

    The MayBe Functor is used to handle potential null or undefined values in a functional way. Its map implementation checks if the contained value is 'nothing' (null/undefined); if so, it skips the transformation function and returns a MayBe containing null, preventing runtime errors like 'cannot read property of null'.
    const MayBe = function (val) {
      this.value = val;
    };
    
    MayBe.of = function (val) {
      return new MayBe(val);
    };
    
    MayBe.prototype.isNothing = function () {
      return this.value === null || this.value === undefined;
    };
    
    MayBe.prototype.map = function (fn) {
      return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));
    };
    
    // Usage: Prevents crashes when value is null
    const result = MayBe.of(null).map(x => x.toUpperCase());
    console.log(result.value); // null (instead of throwing an error)
  4. Compose functions with multiple arguments using partial application

    Since standard compose typically expects functions that take a single argument, functions requiring multiple arguments (like filter or map) must be converted into single-argument functions using partial application before they can be composed.
    let filterGoodBooks = (book) => book.rating[0] > 4.5;
    let projectTitleAndAuthor = (book) => ({ title: book.title, author: book.author });
    
    // Use partial to fix arguments so they only take one value (the data collection)
    let queryGoodBooks = partial(filter, undefined, filterGoodBooks);
    let mapTitleAndAuthor = partial(map, undefined, projectTitleAndAuthor);
    
    // Now they can be composed
    let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor, queryGoodBooks);
    console.log(titleAndAuthorForGoodBooks(apressBooks));
  5. Implement and use basic function currying

    Currying is the process of transforming a function that takes multiple arguments into a series of nested functions that each take a single argument (unary functions). This allows you to fix some arguments and create a more specialized function to be reused.
    // Standard multi-argument function
    const add = (a, b) => a + b;
    
    // Curried version
    const addCurried = a => b => a + b;
    
    // Usage
    const addFour = addCurried(4); // Returns a function: b => 4 + b
    console.log(addFour(3)); // 7
    console.log(addCurried(4)(3)); // 7
  6. Use the Either Functor to handle branching error states

    The Either Functor is used to distinguish between a successful result (Some) and a failure/error state (Nothing). Unlike MayBe, which simply ignores nulls, Either allows you to carry error information in the Nothing instance. The Nothing implementation of map does nothing and simply returns itself, effectively short-circuiting the chain of operations if an error has occurred.
    const Nothing = function (val) {
      this.value = val;
    };
    Nothing.of = function (val) { return new Nothing(val); };
    Nothing.prototype.map = function (f) { return this; };
    
    const Some = function (val) {
      this.value = val;
    };
    Some.of = function (val) { return new Some(val); };
    Some.prototype.map = function (fn) {
      return Some.of(fn(this.value));
    };
    
    const Either = { Some, Nothing };
    
    // Usage
    const response = (type) => {
      return type === 'success' 
        ? Either.Some.of({ data: 'OK' }) 
        : Either.Nothing.of({ error: '404 Not Found' });
    };
    
    const result = response('fail').map(res => res.data);
    console.log(result); // Nothing { value: { error: '404 Not Found' } }
  7. Implement partial application for non-sequential arguments

    While currying works sequentially, partial application allows you to fix specific arguments regardless of their position. By using undefined as a placeholder, you can specify which arguments should remain dynamic and be filled in during the final call.
    const partial = function (fn, ...partialArgs) {
      let args = partialArgs.slice(0);
      return function (...fullArguments) {
        let arg = 0;
        // Fill in the placeholders (undefined) with the provided arguments
        for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
          if (args[i] === undefined) {
            args[i] = fullArguments[arg++];
          }
        }
        return fn.apply(this, args);
      };
    };
    
    // Example 1: Fixing the timeout value in setTimeout (where time is the 2nd arg)
    // setTimeout(callback, delay) -> we want to fix delay=10
    const delayTenMsPartial = partial(setTimeout, undefined, 10);
    delayTenMsPartial(() => console.log("Executed after 10ms"));
    
    // Example 2: Creating a pretty-print JSON function
    // JSON.stringify(value, replacer, space) -> fix replacer=null, space=2
    const prettyPrintJson = partial(JSON.stringify, undefined, null, 2);
    console.log(prettyPrintJson({ foo: "bar" }));
  8. Create a generic currying utility function (curryN)

    The curryN utility allows any function to be converted into a curried version. It recursively collects arguments until the number of provided arguments matches the original function's length (fn.length), at which point the original function is executed.
    const curryN = (fn) => {
      if (typeof fn !== 'function') {
        throw Error('No function provided');
      }
      return function curriedFn(...args) {
        // If not enough arguments are provided, return a function to collect more
        if (args.length < fn.length) {
          return function () {
            return curriedFn.apply(null, args.concat([].slice.call(arguments)));
          };
        }
        // Once enough arguments are collected, execute the original function
        return fn.apply(null, args);
      };
    };
    
    // Example: Creating specialized loggers
    const loggerHelper = (mode, initialMessage, errorMessage, lineNo) => {
      if (mode === "DEBUG") console.debug(initialMessage, errorMessage + " at line: " + lineNo);
      else if (mode === "ERROR") console.error(initialMessage, errorMessage + " at line: " + lineNo);
      else if (mode === "WARN") console.warn(initialMessage, errorMessage + " at line: " + lineNo);
      else throw "Wrong mode";
    };
    
    const curriedLogger = curryN(loggerHelper);
    const errorLogger = curriedLogger("ERROR")("Error At Stats.js");
    
    // Now only need to provide the remaining specific arguments
    errorLogger("Error message", 21);
  9. Debug complex compositions using an identity function

    To inspect intermediate values in a compose chain without altering the data flow, insert an identity function. Because compose executes right-to-left, place the identity function immediately to the left of the function whose output you wish to inspect.
    const identity = (it) => {
    	console.log(it);
    	return it;
    }
    
    // To see the result of splitIntoSpaces, place identity before count
    composeN(oddOrEven, count, identity, splitIntoSpaces)("Test string");
  10. Implement a data pipeline using pipe

    A pipe is the opposite of compose. It processes data from left to right, mirroring the behavior of Unix pipes (e.g., ps -ef | grep nginx). This is often more intuitive for developers as it follows the natural reading order of data transformation.
    const pipe = (...fns) => {
    	return (value) => {
    		return reduce(fns, (acc, fn) => fn(acc), value);
    	}
    }
    
    // Usage: Left-to-right execution
    // splitIntoSpaces -> count -> oddOrEven
    let oddOrEvenWords = pipe(splitIntoSpaces, count, oddOrEven);
  11. Create a unary function wrapper

    The unary higher-order function ensures that a function only receives one argument. This is particularly useful when passing functions like parseInt to Array.prototype.map, as map passes three arguments (value, index, array) to the callback, which can lead to unexpected results in functions that accept multiple parameters.
    const unary = (fn) => {
      return fn.length === 1 ? fn : (args) => fn(args)
    }
    
    // Without unary: ['1','2'].map(parseInt) => [1, NaN]
    // With unary: correctly converts strings to integers
    ['1', '2'].map(unary(parseInt)); // [1, 2]
  12. Implement a custom map function for arrays

    The map function iterates over an array, applies a provided processing function fn to each element, and returns a new array containing the results.
    const map = (array, fn) => {
    	let results = []
    	for (const value of array)
    		results.push(fn(value))
    	return results;
    }
    
    let ret = map([1, 2, 3], (it) => it * it);
    // => [1, 4, 9]