Graduate Program KB

Functional Light JavaScript

Table of Contents

Function Purity
Argument Adapters
Point Free
Closure
Composition
Immutability
Recursion
List Operations
Transduction
Data Structure Operations
Async


Function Purity

  • Just using the function keyword isn't enough for something to be a function.
  • The importance lies in the function call rather than the declaration.
  • Function purity is a level of confidence, it's not binary (pure or not pure).

What is a Function?

As a general rule:

  • A function has to take input and return some output.
  • What's outputted has to be obviously correlated in some fashion to the input.

A function is the semantic relationship between input and computed output.

Procedure vs Function

  • A procedure is a collection of operations, a function is a little more than this.
  • A "function" without a return keyword is a procedure not a function.
  • Functions can't call procedures and still be a function.
    • If a function calls a procedure that function then becomes a procedure.
  • The point of this distinction is that in order to do functional programming, you need to be using functions... not procedures.

Side Effects

  • Functions can't have side effects in order to be a function.
  • To avoid side effects, everything that the function does should be confined within that function.
  • It should not interact with code outside of itself.
  • We want to avoid these side effects (if possible) with the function call.
  • Some examples of side effects include:
    • I/O (console, files, etc.)
    • Database Storage
    • Network calls
    • DOM
    • Timestamps
    • Random numbers
    • CPU Heat
  • It is important to understand that you can't avoid all of these, it's theoretically impossible (CPU heat as an example). The goal is to MINIMISE side effects.
  • There is an exception in terms of accessing external variables.
    • If the external variable acts as a constant (ie. you're not changing anything external) it is okay to use this in your functions and still be following functional programming principles.

Pure Functions

  • Is a function that takes direct inputs and produces direct outputs with no side effects.
  • We are chasing pure function calls.
  • To obtain a pure function call we call a function that obeys the first dot point in this sub section.
  • A function call is pure if it has referential transparency, it can be replaced by its return value and not affect the rest of the program.

Reduce Surface Area

  • Sometimes decreasing the surface area can improve readability.
  • To demonstrate this look at below snippet:
function addAnother(z) {
    return function addTwo(x, y) {
        return x + y + z;
    };
}

addAnother(1)(20,21); // 42
  • Here addTwo is referencing a variable outside of itself (z).
  • This could lead to a potential side effect, however now there are only 2 places we need to check in order to make sure that z is being treated as a constant in the context of this program.
    • That is within the addAnother scope or addTwo scope.
  • We have reduced the surface area for a side effect.

Adapter Functions

  • If we have a third-party that we have to use, we can capture state, do our operations or computations, save this in a variable, mutate our variable back to its original state, and finally return the new value.
  • This is messy and not always worth it, but none the less a handy tool to have.

Wrapper Functions

  • We can wrap functions in other functions to create pure functions by utilizing closure.
function getStudentsByName(students) {
	students = students.slice();
	return sortStudentsByName();

	function sortStudentsByName() {
		// Don't modify this function
		students.sort(function byName(s1,s2){
			if (s1.name < s2.name) return -1;
			else if (s1.name > s2.name) return 1;
			else return 0;
		});
		return students;
	}
}

Argument Adapters

  • Parameter and Arguments are fundamentally different; parameter is what is in the function definition and arguments refers to the actual values that are parsed into the function.

  • The point of argument adaptors are to alter the shape of our functions by altering the arguments of our functions with higher order functions.

  • Different shapes of functions:

    // unary
    function increment(x) {
        return sum(x, 1);
    }
    
    // binary
    function sum(x, y) {
        return x + y;
    }
    
    • Unary functions are preferred for the functional programmer, binary coming second best.
  • Adaptors tend to involve higher order functions to alter function shape.

  • We can change function from unary to binary for example.

  • Here is an example:

function flip(fn) {
    return function flipped(arg1, arg2, ...args) {
        return fn(arg2, arg1, ...args);
    };
}

function f(...args) {
    return args;
}

var g = flip(f);

g(1,2,3,4); // [2,1,3,4]

Point Free

  • A function without explicitly defining its inputs.
  • Passing a function into a function for it to handle.
  • Look at the following code and notice how both blocks do the same thing.
getPerson(function onPerson(person) {
    return renderPerson(person);
});

getPerson(renderPerson);
  • This is achievable due to onPerson and renderPerson being the same shape and therefore being interchangeable.
    • This can be referred to Equational Reasoning.
  • notice how now there's no point, it is point free. You are passing a function into a function with no arguments.

Closure

  • Closure is when a function "remembers" the variables around it even when that function is executed elsewhere.
  • Allows a you to run functions in other scopes and the function will still have access to all its variables. This is thanks to closure.
  • Closure can often be a reason for functional impurity. You have to make sure that you're not mutating variables that are external but in your closure.

Eager vs Lazy Evaluation

  • It's important to be aware of how things are being evaluated.
  • Usually if we are returning something that is doing work, this will be lazy evaluated, whereas just about everywhere else will be eagerly evaluated.
  • Below the work is evaluated lazily, the "work" happens every function call due to the operation happening at the return statement.
    function repeater(count) {
        return function allTheAs() {
            return "".padStart(count, "A");
        };
    }
    var A = repeater(10);
    A(); // work is done here
    A(): // and here
    
  • The next block has the "work" eagerly evaluated:
    function repeater(count) {
        var str = "".padStart(count, "A");
        return function allTheAs() {
            return str;
        };
    }
    var A = repeater(10); // work is done here
    A();
    A():
    

Memoization

  • Allows us to not repeat work and only do work when its asked for.
  • In some way, shape or form store a means to know what we have previously evaluated in order to avoid doing it again.
  • Here is an example for the case above:
    function repeater(count) {
        var str;
        return function allTheAs() {
            if (str == undefined) {
                str = "".padStart(count, "A");
            }
            return str;
        };
    }
    var A = repeater(10);
    A();
    A();
    
  • The above code gives us the best of both worlds and is in fact a pure function.
    • However, notice that it is not obviously pure, you have to look into a bit to realise this.
  • We can use the memoize keyword to change the implementation to make it more obvious.
function repeater(count) {
    return memoize(function allTheAs() {
        return "".padStart(count, "A");
    });
}

Partial Application

  • takes a function as the first input and takes the parameters to pass this function as the second argument.
function ajax(url, data, cb) {/***/}

var getCustomer = partial(ajax, CUSTOMER_API);
var getCurrentUser = partial(getCustomer, {id:42});

getCustomer({id:42}, renderCustomer);
getCurrentUser(renderCustomer);

Currying

function ajax(url) {
  return function getData(data) {
    return function getCB(cb){/**/}
  }
}

ajax(CUSTOMER_API)({id:42})(renderCustomer);

var getCustomer = ajax(CUSTOMER_API);
var getCurrentUser = getCustomer({id:42});
var ajax = curry(
  3,
  function ajax(url, data, cb){/**/}
)
  • One of the main applications with currying is to change the shape of a function.

  • Below we want to perform an addition on every item in an array, but map only takes unary functions, so we use use our binary function inside a unary arrow function by presetting one of the parameters.

  • See below; we can do the first part in one easy step using curry():

    // Partial Application
    function add(x, y) {
      return x + y;
    }
    
    [0,1,2].map(function addOne(v) {
      return add(1, v);
    });
    // [1,2,3]
    
    // Currying
    add = curry(add);
    
    [0,1,2].map(add(1));
    // [1,2,3]
    

Partial Application vs Currying

  1. Both are specialization techniques.
  2. Partial application presets some arguments now, receives the rest on the next call.
  3. Currying doesn't preset any arguments, receives each argument one at a time.

Composition

  • Involves passing a function call into another function.

  • Composition is associative.

    • function3(function2(function1)) or something like inc(inc(inc(3))).
  • It can be very helpful to abstract compositions:

    function minus2(x) { return x - 2; }
    function triple(x) { return x * 3; }
    function increment(x) { return x + 1; }
    
    function shippingRate(x) {
      return minus2(triple(increment(x)));
    }
    
    totalCost = basePrice + shippingRate(4);
    
  • We can create different data flows by abstracting even further:

    • Compose: Right to Left
    • Pipe: Left to Right
    function composeThree(f3, f2, f1) {
      return function compose(f) {
        return f3(f2(f1(v)));
      };
    }
    
    function pipeThree(f1, f2, f3) {
      return function pipe(v) {
        return f1(f2(f3(v)))
      };
    }
    

Immutability

  • Is all about controlling change.
  • assignment immutability: once a variable is assigned, it can't be reassigned.
    • Is what the const keyword is for.
  • Primitive values are immutable by nature, this is important to realise as when we "change" a variable holding a primitive value, we are actually reassigning it.
  • value immutability: values like arrays and objects can easily be changed, value immutability means that the value can't change (with primitives in javascript this is not a worry, you can't change a primitive, you're reassigning the value.)
  • We can make sure that a value is immutable by making it read only with Object.freeze().
    • Adds readability value and it actually throws an exception if something tries to mutate the value.
  • Should always assume that the object is read only, in your function if you need to do a mutation, make a copy and mutate it internally.
    • No changes (side effects) to outside code.
  • immutable data structures: need to be mutated in a controlled manner.

Recursion

  • Functions calling themselves.
  • Stack frame: depending on how we are doing our recursion determines how the stack evolves, each call of the function can be thought of as a stack frame.
    • If our return is purely a recursive call then we don't need to add each stack frame to the stack, this is tail recursion return functionCall()
    • If our return is returning something on top of the recursive call, then we need to add each stack frame to the stack so we can keep track of each return statement value so that we can compute once all recursive calls have been completed. return value + functionCall()
  • Proper Tail Calls (PTC): a tail call gets memory optimised
  • Tail Call Optimisation (TCO): A family of potential optimisations on top of PTC which are optional that a engine may decide that it wants to do.

Trampolines

  • Indicates that we want something to bounce back and forth.
  • Instead of making a recursion call we return a function that will make the next call.
  • Considered a better form of recursion.
function trampoline(fn) {
  return function trampolined(...args) {
    var result = fn(...args);

    while (typeof result == "function") {
      result = result();
    }
    return result;
  };
}
let countVowels =
  trampoline(function countVowels(count, str) {
    count += (isVowel(str[0]) ? 1 : 0);
    if (str.length <= 1) return count;
    return function f() {
      return countVowels(count, str.slice(1));
    };
  });

// optionally: here is same result with curry
countVowels = curry(2, countVowels)(0);

List Operations

  • map: a transformation operation, takes values and performs some operation on these values.

    • Will always return the same data structure just with the values changed.
    • The map callback takes 3 arguments: name, index and array.
    • We can destructure objects in map by using the {} where the parameters go, this allows us to look at certain object properties.
  • filter: includes values if they return true and excludes them if they return false.

    • Takes a predicate function that determines whether a value will return true or not.
  • reduce: combines values.

    • You choose how the values combine, it could be anything.
  • We can compose functions within these calls if they are pure functions and have the same shape.

function add1(v) { return v + 1};
function mul2(v) { return v * 2};
function div3(v) { return v / 3};

let list = [1, 2, 3, 4, 5, 6, 7];
list.map(
  compose(div3, mul2, add1) // add1, mul2, div3
);

Transduction

  • is composition of reducers.
  • allows us to compose functions of different shapes.
  • The transduce method is common among functional programming libraries and consists of 4 key inputs:
    1. transducer (the transformations)
    2. reducer
    3. initial value
    4. what you want to operate on (array)
function add1(v) { return v + 1; }
function isOdd(v) { return v % 2 == 1; }
function sum(total, v) { return total + v; }

// Can compose many functions together only thing to remember is to use mapReducer on transformations and filterReducer for predicates / boolean checks.
let transducer = compose(
  mapReducer(add1);       // these functions are provided by functional libraries
  filterReducer(isOdd);
);

transduce(
  transducer, // transducer which we made above; the transformations
  sum,        // reducer
  0,          // initial value
  [1,2,3,4,5] // what we operate on
);


Data Structure Operations

  • We can apply operations such as map, filter and reduce to data structures as well. See below example:
let obj = {
  name: "Kyle",
  email: "mail@gmail.com"
};

function mapObj(mapper, o) {
  let newObj = {};
  for (let key of Object.keys(o)) {
    newObj[key] = mapper( o[key] );
  }
  return newObj;
}

mapObj(function lower(val) {
  return val.toLowerCase();
}, obj);
  • A monad is a wrapper around a value with a set of behaviors in it which makes the value easy to interact with other values in a specific way.
    • It turns the value into a functor. (A monad produces a monad)
    • Kyles Definition of a Monad: a pattern for pairing data with a set of predictable behaviors that let it interact with other data+behavior pairings (monads).
function Just(val) {
  return { map, chain, ap };

  function map(fn) {
    return Just(fn(val));
  }

  // aka. bind, flatMap
  function chain(fn) {
    return fn(val);
  }

  // To do this your value needs to be a function, due to map requiring a mapper function.
  function ap(anotherMonad) {
    return anotherMonad.map(val);
  }
}
  • flat map is a way of flattening out a data structure. Imagine a 2D array and you want all the values but just as a one dimensional array... flat map!
  • Below is how we can use it (quite complex way of thinking):
let user1 = Monad("kyle");
let user2 = Monad("susan");

let tuple = curry(2, function tuple(x, y) {
  return [x, y];
});

let users = user1.map(tuple).ap(user2);

users.chain(identity); // ["kyle", "susan"]
  • There is a way we can deal with null and undefined in Monads with the Nothing Monad.
function Nothing() {
  return { map: Nothing, chain: Nothing, ap: Nothing };
}

let Maybe = { Just, Nothing, of: Just };

function fromNullable(val) {
  if (val == null) return Maybe.nothing();
  else return Maybe.of(val);
}

Async

  • All of these functional programming principles can be applied in asynchronous programming and can be very useful.
  • A popular library is Rx.js

Key Takeaways

  • Whenever you see a function ask what shape is it? (reducer?) What does this allow me to do with it?

Return