Understanding Functional Programming

SHAIK IRFANSHAIK IRFAN
12 min read

Why FP:

Paradigm shift in the way that we approach code, can be incorporated on a high level as Imperative vs Declarative.

Imperative focuses on how something is being done, In imperative coding style, future reader of the code has to read all of the code and in a sense mentally execute the code to understand the purpose of the code, Infer form the code by reading (dry run) the entire code focusing more on how is the code executing and then only understand what the code is doing.

The process of reading the code in the sense to understand how the code is working is not ideal scenario, as humans are not very well in terms of such tasks when compared to machines which is their sole task.

Any code written which need to be read and mentally understood by the user, just so that they can use it or understand it, is harder to understand and code which is harder to understand is harder to maintain, to fix, to improve.

Declarative does not focus oh how, or rather we can put is as, It abstracts the how factor, and focuses on what (outcome) and why factors.

Code comments should not be the duplicate or just a narration of what the code is doing, for example if there is a code i++ the the comment should not be incrementing i by one instead it should focus on why incrementing i by one.

Make the code obvious why it is doing something

Javascript Functions

A Javascript function is a set of instructions that performs a specific task wrt to their lexical scope chain when called upon. Functions are used to break down a program into smaller, reusable pieces.

A function in ECMAScript is defined as a callable object that has a [[Call]] internal method (AKA functions are first-class).

ECMAScript recognises different types of functions, including:

  • Regular functions defined with function keyword.

  • Arrow functions (=> syntax).

  • Generator functions (function* syntax).

  • Async functions (async function syntax).

Side Effects:

if a function has any in-direct I/O which are effected on it's call, then the effects of such function execution are called side effects.

A pure function should take direct inputs do computation and return the result without effecting anything out-side. It can't access anything outside of itself and it can't assign anything outside of itself. close but not complete.

There is no such thing as a function there is only function call, to have a definition is not an important part but the important part is that the function is called at specific point to which we gave direct input and got direct output. Avoid side effects and side causes with function call if possible??

Now even if we access indirect-input if that in-direct input can be clearly interpreted as a constant then such function is clearly still valid as a pure function.

Side effects have become a part of modern day programming

// f(x) = x^2 + 2
let x = 2;
let y;
function callMe() {
     y = x^2;
}

A pure function should not have any side effects

Pure Functions?:

take inputs return outputs. if output is an object or an array then each individual element must also be same within that object or array. (think in terms of de-structuring)
Schematic relation between input and output.

A pure function takes some inputs dome some computation in relation with those inputs and then return same outputs for those given inputs on every call. If no return then not a function.

the inputs and outputs must be direct, ie without any sideEffetcs(output), sideCauses(input)

pure functions can only call pure functions, if not they become impure.

only pure functions can take advantages of functional programming.

still we can use the function api but we don't get the provability, we don't get the verification, the security of thing thing is definitely going to do what we wanted to do.

ECMAScript Language Specification does not explicitly differentiate pure functions. So by design specifications there is no way to determine weather a function is pure, So what now ?

What are pure functions:

function AccumulateArrayOps(list, cb, accumulator=0, index=0){    
    if (index === list.length) {
        return accumulator;
    }
    return AccumulateArrayOps(
        list,
        cb,
        cb(accumulator, list[index], index, list),
        index + 1
    )
}

const sumofArray = AccumulateArrayOps([1,2,3,5], (acc, cv) => acc += cv);
function callEachWithCallback(arr, cb){
    if(arr.length === 0) {
        return[]
    }
    const [each, ...rest] = arr
    const cbForEach = cb(each)
    const mapOverRest = _map(rest,cb)
    return [cbForEach, ...mapOverRest]
}
// f(x) = x^2 + 2
function parabola(x) {
    return x^2
}
// f(x,y) = 2x^2 + 3y + 4
function hyperbola(x,y) {
    return 2x^2 + 3y + 4
}

Pure Functions with constants:
if we have a constant which is outside the program and it never changes then a function using such constant as in-direct input can be still considered as a pure function but for a reader to understand that he will have to go through the entire program.
so we can reduce the surface area of where this change can happen and make it more pure by increasing readability and confidence.
Function purity is not a binary characteristic, it metric of how confident are we the reader in the functions behaviour.

Fundaments FP techniques:

  • extracting impurity

  • containing impurity

Function Adapters:

categorised as unary, binary, nary

Shape Adapters:When a functions need to be given a specific shape, then we create a hof which takes the the og funtion as input and returns a shaped function which calls the og function with required shape.

so basically we receive a function and return a new adapter function over the OG function with desired shape, which is now.

we are creating a function to call another function, so calling function directly or calling it's adapter is also same but with adapter we can give a specific shape to the original function.

while we can create adapter for all and any kind of function, but we should look for common patterns for adaption which are widely recognised, if an adapter is not common or not self-explanatory then it can hinder the readability and confidence which is not what we are looking to do in FP

Point Free:

It is a style of defining a function by defining other functions, without needing to define it's points. I.e without needing to explicitly define and map it's inputs can we define a function.

essentially it is a style of writing functions, we define functions without writing anything inside them.

making a function by making other functions.

Equational Reasoning:

if we have more than one inputs, we create a hof which takes one input and fixes it to the OG function inside HOF which gets returned.

function processor(v){
   return v*2
}
function createProcessor(callback) {
    return function process(input) {
        return callback(input);
    };
};
function processFor(value) {
    return function processWith(cb){
        return cb(value)
    }
}
const processAdapter = createProcessor(processor);
const pointFreeProcesswith10 = processFor(10)

pointFreeProcesswith10(processor);

(10);

processor(10);

From the above functions implicitProcessor and processor are referentially transparent for a given specific input so they can be interchanged as they can be equationally reasoned.

For given specific input 10 then all the three functions processWith10(processor) ,implicitProcessor(10); , Processor(10); can be equationally reasoned.

A point free function is a function which can be defined without needing to define its inputs(points), It is a technique for defining functions by the way of other functions

function mod(x) {
    return function with(y) {
        return x%y
    }
}

function equal(x) {
    return function to(y) {
        return x === y
    }
}

function compose(fn1,fn2){
    return function composeWith(v){
        return fn2(fn1(v))
    }
}
const isOdd = compose(mod(2),equal(1))

function not(cb){
      return function(...args){
        return !cb(...args)
    }
}

isOdd(3)

const isEven =  not(isOdd)
isEven(4)

map(cb)

Closure:

The scope of function definition remains accessible within the scope of that function call irrespective of the scope of function call.

Always try to maintain purity over the closured data and only close-over non-mutating data.

function addStr(str){
    return function next(v){
        if(typeof v === 'string'){
            return addStr(str + v)
        }
        return str
    }
}

Lazy vs Eager Execution

Lazy or defer is a case where the result of the function is not computed immediately on it's call rather it get's deferred by returning an extra function wrapper whose call will compute the result.

function add(a,b){
    return function result(){
        return a + b;
    }
}

const add2_3 = add(2,3)
add2_3()
add2_3()

Eager

function add(a,b){
    const addition = a+b;
    return function result(){
        return addition;
    }
}

const add2_3 = add(2,3)
add2_3()
add2_3()

Memoization

function addMemo(a,b) {
    let addition;
    return function result(){
        if(!addition){
            addition = a+b;
        }
        return addition;
    }
}

Referential Transparency:

If a return value of a function call can be replaced with that function call itself then it is referentially transparent.

A function is pure if it has referential transparency. ECMAScript spec does't specify about the functional purity and so the compiler does not natively support referential transparency, although with the introduction of JIT compilation, functional programming paradigm can aid optimising the compilation procedure.

Referential transparency infills the high degree of confidence and increases the reasoning and readability of our codebase significantly.

Generalised to Specialised.

async function networkRequest(url,id,cb){
    const response = await fetch(`${url}/posts/${id}`);
    const data = await response.json();
    return cb(data);
}

await networkRequest('https://www.uri.com',123,DisplayUser);

function getUserPosts(id, cb){
    return networkRequest('https://www.uri.com',id,cb);
}

await getUserPosts(123,DisplayUser);

async function getCurrentUserPosts(cb){
    return await networkRequest('https://www.uri.com',123,cb);
}
await getCurrentUserPosts(DisplayUser);

When specialising the functions the parameter order matters in a way that we need to specialise by adapting over the most general to most specific parameters.

Manually making function more specialised by defining adaptor functions, makes our code cluttered. There are two main alternatives which help us to make our functions more specialised called partial application and currying.

Partial Application:
let's take the original generic implementation of networkRequest function

async function networkRequest(url,id,cb){
    const response = await fetch(`${url}/posts/${id}`);
    const data = await response.json();
    return cb(data);
}
networkRequest('https://www.uri.com', 123, renderUser)

const getUserPosts = partial(networkRequest, 'https://www.uri.com')
getUserPosts(123, renderUser)

const getCurrentUserPost = partial(getUserPosts, 123)
getCurrentUserPost(renderUser)

Currying:

function networkRequest(url){
    return function getUserPosts(id){
        return function render(cb){
           return async function makeRequest(){
                const response = await fetch(`${url}/posts/${id}`);
                const data = await response.json();
                return cb(data);
            }
        }
    }
}

const abc = await networkRequest("")(123)(function renderUser(data){
    // stuff
    return data;
})()

const curryUserPosts = curry(4,async function networkRequest(url,id,cb){
    const response = await fetch(`${url}/posts/${id}`);
    const data = await response.json();
    return cb(data);
})

const abc = await response("")(123)(function renderUser(data){
    // stuff
    return data;
})()

Both Partial Application and currying specialise the given function

Partial application adapts over the function calls, which means it returns an more specialised adapter function over the og function's call by applying the given argument. So partial application takes a function and a presets one or more arguments then return a more specialised function.

Currying creates a wrapper adapter function which keeps returning a function that expects next inputs until we have provided the specified number of inputs. Currying does not preset any arguments, it expects arguments for each call and returns a function which is ready take next set of arguments until we have provided all of the required arguments.

Shape Adaption using currying:

function add(x,y){
    return x + y;
}

const add = curry(add)

[1,2,3].map(add(1))

The add function is curried resulting in a more specialised function and also now has a new shape, If we can look into the map function which takes a callback, the shape of callback and the curried add function are same so we can do equational reasoning and substitute the new curried function in-place of the callback.

Composition:

Composition is the idea of one function's output becoming the input to another function. Composition makes data flow more declarative, It is a flow of data between series of operations(function call) defined declaratively rather than imperatively.

function addCharges(x){
    return x + 200;
}

function doubleQty(x){
    return x * 2;
}

function minusDiscount(x){
    return x - 100;
}

const composeShipping = compose(addCharges,doubleQty,minusDiscount);
const pipeShipping = pipe(minusDiscount, doubleQty, addCharges);

composition is associative f ∘ (g ∘ h) = (f ∘ g) ∘ h. Since the parentheses do not change the result, they can be omitted. which means we can curry our composition it is very useful as we don't need to know all of the function which we are going to compose before-hand.

function composeTwo (fn1,fn2) {
    return function composed(x){
        return fn2(fn1(v));
    }
}

const g = composeTwo(composeTwo(addCharges,doubleQty),minusDiscount);
const h = composeTwo(addCharges,composeTwo(doubleQty,minusDiscount));

Composition with Currying:

Composition must be done with unary functions as generally function outputs a single result and it can't be compose over a function which is taking multiple inputs.

If we want to compose functions which are not unary then we need to specialise them to unary functions using currying/partial application and then do composition over them.

Let's try to re-write the even odd function now using currying and composition

function mod(x,y){
 return x % y;
}

function equal(x,y){
    return x === y
}

const mod = curry(mod)
const equal = curry(equal)
const isEven = compose(equal(1),mod(2))
const isOdd = not(isEven)

Immutability:

Immutability refers to not changing the state unexpectedly or via sideEffects, It does not mean that state should never change, as the whole point of a programme is to change state over it's execution, But the change must be intentional, controlled and predictable rather than an un-intentional.

Assignment Immutability:

When some data is assigned to an identifier then the state of that identifier can't be re-assigned

const gives us assignment Immutability

Value Immutability:

Primitive values are immutable, they can't be altered but only be re-assigned. Where as non-primitive values like objects, arrays, functions can be mutated without any re-assignment and const doesn't provide any security against value immutability. We as a developer have to account for value mutability.

Object.freeze:

to make the data read-only we can call Object.freeze on the non-primitive data. Object.freeze applies shallow read-only, it doesn't apply to nested non-primitive data.

Avoid mutation and copy:

When there is a function which takes non-primitive data as arguments then it is ideal to copy and create a new instance of such data rather than mutating it directly.

Immutable Data Structures:

Immutable data structures imply that mutation should be done in a controlled manner, where as if there is no need to mutate the it is a read-only data structure.

It is a representation of data structures which we are used to dealing with like arrays and objects, but we don't have access to the actual underlying data structure, we only have API's to access it. The API creates a layer of control that prevents unexpected changes of the data structure.

Immutable Data structure essentially says that we cannot change the data structure but we can create a new data structure by applying the required changes.

0
Subscribe to my newsletter

Read articles from SHAIK IRFAN directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SHAIK IRFAN
SHAIK IRFAN