Skip to content

Instantly share code, notes, and snippets.

@jaunkst
Last active January 7, 2021 18:06
Show Gist options
  • Save jaunkst/6f8842364ef98cf4898a8a75f2f8b5f5 to your computer and use it in GitHub Desktop.
Save jaunkst/6f8842364ef98cf4898a8a75f2f8b5f5 to your computer and use it in GitHub Desktop.

A Basic Introduction

Pure Functions (x → y)

A pure function takes in arguments and returns values, uses no hidden state, has no side effects, is referentially transparent, easy to reason, and test.

Functional Programming

The functional programming paradigm was explicitly created to support a pure functional approach to problem solving. Functional programming is a form of declarative programming. In contrast, most mainstream languages, including object-oriented programming (OOP) languages such as C#, Visual Basic, C++, and Java, were designed to primarily support imperative programming.

Imperative programming gives rise to a lot of the constructs we use every day: control flow (if-then-else statements and loops), arithmetic operators (+, -, *, /), comparison operators (===, >, <, etc.), and logical operators (&&, ||, !).

FP Practices

Avoid Mutating State

A function that mutates state outside of its scope is considered dangerous and can lead to bugs and unknown side effects as the application becomes more complex.

let mutatedState = 1;

function example() {
    mutatedState += 1;
}

example();

console.log(mutatedState);

Avoid Shared State

Shared state happens as your application grows and relies on mutations to change the applications state. When many functions share state it can make reasoning about the application state difficult.

let sharedState = 1;

function exampleA() {
    sharedState += 1;
}

function exampleB() {
    exampleD();
    sharedState += 2;
}

function exampleC() {
    sharedState -= 2;
}

function exampleD() {
    exampleA();
    sharedState += 3;
    exampleC();
}

exampleA();
exampleC();
exampleB();
exampleD();

console.log(sharedState);
// 6 <- how did we get here?

Avoid Unnecessary Side Effects

Examples:

  • Changing the value of a variable.
  • Writing some data to disk.
  • Enabling or disabling a button in the User Interface.

It's really very simple. Side effect = changing something somewhere.

Applications cannot avoid side effects but being mindful of 'when', and 'where' they happen. Always be clear about the event.

Notice: In our examples for the purpose of showing what is happening we will be using console.log which is actually a side effect and not considered pure.

  • Solve problems with verbs (actions)
  • Think in terms of transformations
  • Use simple data structures
  • Use simple functions

Functions As Behaviors

Even the simplest behavior can become a function. Constructs we take for granted can be abstracted into functions.

if else, switch, not, and, or, loops, existence, property accessors, ect..
const prop = (key) => {
    return (obj) => {
        return obj[key];
    }
}

const not = (val) => {
    return !val;
}

Why would I even? because little, general, pure functions are composable

Currying

Currying is a way of constructing functions that allows partial application of a function’s arguments. What this means is that you can pass all of the arguments a function is expecting and get the result, or pass a subset of those arguments and get a function back that’s waiting for the rest of the arguments.

const addBy = function(byAmount) {
    return function(value) {
        return value + byAmount;
    }
}

addBy(20)(2)
// -> 22

const addOne = addBy(1);
const addTen = addBy(10);

addOne(2);
// -> 3

addTen(2);
// -> 12

Simplest Compose & Decorators

Compose & Decorators are higher-order pure functions that take only functions as arguments and return a function.

const composeTwo = function(b, c){
    return function(a){
        return c(b(a));
    };
}

const before = function(decorator){
    return function(target){
        return function(){
            decorator.apply(this, arguments);
            return target.apply(this, arguments);
        }
    }
}

const after = function(decorator){
    return function(target){
        return function(){
            var val = target.apply(this, arguments);
            decorator(val);
            return val;
        }
    }
}

Example of using them together.

// with currying
const logBefore = before(console.log);
const logAfter = after(console.log);
const logAddByOne = logBefore(logAfter(addBy(1)));

logAddByOne(2)
// before -> 2
// after -> 3

// with compose
const logBeforeAndAfter = composeTwo(logBefore, logAfter);
const logAddByOneComposed = logBeforeAndAfter(addBy(1));

logAddByOneComposed(2);
// before -> 2
// after -> 3

Chain

You have probably used this pattern in lodash, underscore or jQuery.

const stooges = [{name: 'curly', age: 25}, {name: 'moe', age: 21}, {name: 'larry', age: 23}];

const youngest = _.chain(stooges)
    .sortBy((stooge) => { return stooge.age; })
    .first()
    .value(); //=> { age: 21, name: "moe" }

Pointfree

The Chain example is not a pointfree pattern. As it 'says' the data (stooge).

const youngest = _.chain(stooges)
    .sortBy((stooge) => { return stooge.age; }) // <- stooge
    .first()
    .value(); //=> { age: 21, name: "moe" }

Chain Pointfree

const prop = (key) => {
    return (obj) => {
        return obj[key];
    }
}

const youngest = _.chain(stooges)
    .sortBy(prop('age')) // <- no stooge
    .first()
    .value(); //=> { age: 21, name: "moe" }

Compose

Compose should be used over chain. Avoiding the dot(.) syntax and allowing for better composition. Note that in this example calls go from (right to left || bottom to top) and that each function feeds its result to the next.

// head accepts an array and returns the first item in the array or null.
const head = (arr) => {
    return arr[0] ? arr[0] : null
}

// sort takes an accessor function and returns a new function that accepts and returns a sorted array.
const sortBy = (accessor) => {
    return (arr) => {
        return arr.sort((a, b) => (accessor(a) > accessor(b)) ? 1 : -1)
    }
}

// prop takes a key, and returns a new function that accepts an object and returns an objects keys value.
const prop = (key) => {
    return (obj) => {
        return obj[key];
    }
}

// compose takes any number of functions and collapses from right to left.
const compose = (...fns) => {
    return fns.reduceRight((prevFn, nextFn) => {
        return (...args) => {
            return nextFn(prevFn(...args))
        }
    }, value => value)
}

const stooges = [{name: 'curly', age: 25}, {name: 'moe', age: 21}, {name: 'larry', age: 23}];

compose(
    head, 
    sortBy(prop("age"))
)(stooges); //=> { age: 21, name: "moe" }

Pipe

Simular to compose but the arguments are reversed for brevitiy. (left to right || top to bottom)

// Compose takes any number of functions and collapses from right to left.
const compose = (...fns) => {
    return fns.reduceRight((prevFn, nextFn) => {
        return (...args) => {
            return nextFn(prevFn(...args))
        }
    }, value => value)
}

// reverse an array 
const reverse = (...args) => {
    return args.reverse();
}

// return a curried compose and reverse its arguments 
const pipe = (...fns) => {
    return compose(...reverse(...fns))
}

// Currying map
const map = (func) => {
	return (arr) => {
		return arr.map(func);
	}
}

// Currying forEach
const forEach = (func) => {
	return (arr) => {
		arr.forEach(func);
	}
}

// A function for the following example
const mapAddByValue = (byValue) => {
	return map((value) => {
		value = value + byValue;
		return value;
	})
}

// Runs the given function with the supplied object, then returns the object.
const tap = (fn: Function) => {
    return (state) => {
        fn.call(fn, state)
        return state
    }
}

const mapAddByOne = mapAddByValue(1);
const mapAddByTen = mapAddByValue(10);

// note the top to bottom call order
const examplePipe = pipe(
	mapAddByOne,
	tap(console.log),
	mapAddByTen,
	tap(console.log)
);

const results = examplePipe([1, 2, 3]);
console.log('Results:', results);

// -> [ 2, 3, 4 ]
// -> [ 12, 13, 14 ]
// => Results: [ 12, 13, 14 ]

Ramda

A practical functional library for JavaScript programmers.

Why Ramda?

A library designed specifically for a functional programming style, that makes it easy to create functional pipelines, that never mutates user data.

  • Ramda emphasizes a purer functional style. Immutability and side-effect free functions are at the heart of its design philosophy. This can help you get the job done with simple, elegant code.

  • Ramda functions are automatically curried. This allows you to easily build up new functions from old ones simply by not supplying the final parameters.

  • The parameters to Ramda functions are arranged to make it convenient for currying. The data to be operated on is generally supplied last.

Everything we need without all the boilerplate and ceremony!

Stooges Revisited

import {pipe, head, sortBy, prop} from 'ramda';

const stooges = [{name: 'curly', age: 25}, {name: 'moe', age: 21}, {name: 'larry', age: 23}];

pipe(
    sortBy(prop("age")), // sort stooges by age
    head // return the first stooge in the sorted array
)(stooges); //=> { age: 21, name: "moe" }

A look at Cond for conditions

// always: Returns a function that always returns the given value.
// T: A function that always returns true. Any passed in parameters are ignored.

import {cond, equals, always, T, map, filter, complement, isNil} from 'ramda';

const checkTemp = cond([
    [equals(0),   always('water freezes at 0°C')],
    [equals(100), always('water boils at 100°C')],
    [T,           always(null)]
]);

fn(0); //=> 'water freezes at 0°C'
fn(50); //=> null
fn(100); //=> 'water boils at 100°C'

// filtering nulls in a pipe
pipe(
    map(checkTemp),
    filter(complement(isNil)) // complement curries not().
)([0, 50, 100]) //=> [ 'water freezes at 0°C', 'water boils at 100°C' ]

Learn Ramda

Its great! http://ramdajs.com/docs/

Functional Reactive Programming with RXJS + Ramda

RXJS allows us to build asynchronous applications. It is the async control flow of our application state. Think of it as functional pipes that can be declarative and reactive.

Observable - The Core of RXJS

Observables are similar to Promises. Except that Observables can be canceled and that an observable can continue to emit values over time. Netflix uses the observable pattern to cancel video streams when navigating through the titles so that unwanted data buffers are no longer a concern for the client.

import {Observable, Subscriber, of} from 'rxjs';
import {catchError} from 'rxjs/operators';
const obs$ = new Observable((obs: Subscriber<any>) => {
    obs.next('Hello subscriber! I am an Observable.');
    obs.next('This is my first emittion.');
    obs.next('This is my second emittion.');
    obs.error('This is my first error');
})
obs$.pipe(
    catchError((e) => {
        // return an observable of error
        return of(e)
    })
).subscribe(console.log)

Promise to Observable (by hand)

import {Observable, Subscriber} from 'rxjs';
import https, {Response} from 'https';

const request$ = new Observable((obs: Subscriber<any>) => {
    https.get('https://swapi.co/api/people/', (res: Response) => {
        let rawData = '';
        res.on('data', (chunk) => { 
            rawData += chunk; 
        });
        res.on('end', () => {
            try {
              const parsedData = JSON.parse(rawData);
              obs.next(parsedData);
              obs.complete();
            } catch (e) {
                obs.error(e.message);
            }
          });
        return obs
    }).on('error', (e) => {
        obs.error(e.message);
    });
})

request$.subscribe(console.log)

Promise to Observable (easy way)

import { from } from 'rxjs';
const observable$ = from(promise);

From

From can convert promises to observables, or create observables from arrays and strings that emits values over time.

import { from } from 'rxjs';
const chars$ = from('Jason');
chars$.subscribe(console.log)
// => J
// => a
// => s
// => o
// => n

const people$ = of(['Jason', 'John', 'Amy']);
chars$.subscribe(console.log)
// => Jason
// => John
// => Amy

Of

Of is like an async always(). An observable that emits a given value.

import { of } from 'rxjs';
const name$ = of('Jason');
name$.subscribe(console.log)
// => 'Jason'
const people$ = of(['Jason', 'John', 'Amy']);
people$.subscribe(console.log)
// => [
//     'Jason',
//     'John',
//     'Amy'
// ]

const boolean$ = of(true);
// => true

Subject

A Subject is a special type of Observable that allows values to be multicasted to many Observers. Subjects are like EventEmitters. Subject do not have an initial state.

import {Subject} from 'rxjs';

interface Person {
    name: string;
    age: number;
}

const people$ = new Subject<Person[]>();

people$.subscribe(console.log);

people$.next([{name: 'Jason', age: 37}]);

//people$.subscribe => [{name: 'Jason', age: 37}]

BehaviorSubject

A variant of Subject that requires an initial value and emits its current value whenever it is subscribed to.

import {BehaviorSubject} from 'rxjs';

interface Person {
    name: string;
    age: number;
}

const people$ = new BehaviorSubject<Person[]>([{name: 'Jason', age: 37}, {name: 'Amy', age: 40}]);

people$.subscribe(console.log);

//people$.subscribe => [{name: 'Jason', age: 37}, , {name: 'Amy', age: 40}]

RXJS Best Practices

Avoid

  • Avoid subscribing to an observable and nexting state to another observable.
  • Avoid memory leaks by closing inactive subscriptions.
const people = new BehaviorSubject<Person[]>([{name: 'Jason', age: 37}, {name: 'Amy', age: 40}]);
const currentPerson = new Subject();

people.subscribe((people) => {
    currentPerson.next(people.first())
})

currentPerson.subscribe(console.log)

Instead

  • Use $ Suffix to annotate observable values
  • Use observable composition
  • Use take(x) to unsubscribe if you only need a number of emissions.
  • Use takeUntil(destroyed$) to unsubscribe if you have a cleanup phase.
  • Use functional patterns in rxjs/operators. (ramda!)
import {map as rxMap, take as rxTake} from 'rxjs/observables';
import {head} from 'ramda';

const people$ = new BehaviorSubject<Person[]>([{name: 'Jason', age: 37}, {name: 'Amy', age: 40}]);

// Observable Composition
const currentPerson$ = people$.pipe(
    rxMap(head)
)

currentPerson$.pipe(take(1)).subscribe(console.log)
const destroyed$ = new BehaviorSubject(false);

currentPerson$.pipe(takeUntil(destroyed$)).subscribe(console.log)

onDestroy() {
    destroyed$.next(true);
}

Import Function Names

When using ramda and RXJS operators I find it best to prefix rxjs operators with rx so that we do not pollute the function names between ramda and rxjs/operators.

import {map, filter} from 'ramda'
import {map as rxMap, filter as rxFilter } from 'rxjs/operators';

When do I use ramda or rxjs/operators?

Use RXJS to for control streams and ramda to transform state or shape.

Observable Composition

Pipe

import { BehaviorSubject } from 'rxjs';
import { take as rxTake } from 'rxjs/operators';
import { where, lt, complements, equals, concat } from 'ramda';

const people$ = new BehaviorSubject<Person[]>([
    {name: 'Jason', age: 37}, 
    {name: 'Amy', age: 40},
    {name: 'John', age: 50},
    {name: 'James', age: 18}
]);

const peopleUnderForty$ = people$.pipe(
    rxMap(where({age: lt(40)}))
);

const peopleUnderFortyNotNamedJason$ = peopleUnderForty$.pipe(
    rxMap(where({name: complements(equals('Jason'))}))
);

people$.subscribe(console.log);

// First Emission
//=> [
    //     {name: 'Jason', age: 37}, 
    //     {name: 'John', age: 50},
    //     {name: 'James', age: 18}
    // ]

people$.next(concat(people$.value, [{name: 'Andy', age: 16}]));

// Second Emission
//=> [
    //     {name: 'Jason', age: 37}, 
    //     {name: 'John', age: 50},
    //     {name: 'James', age: 18},
    //     {name: 'Andy', age: 16}
    // ]

CombineLatest

CombineLatest accepts any number of observables, and will emit an array of emitted values from each combined observable.

import { BehaviorSubject, combineLatest } from 'rxjs';
import { map as rxMap, tap as rxTap } from 'rxjs/operators';
import { concat, flatten } from 'ramda';

const peopleA$ = new BehaviorSubject<Person[]>([
    {name: 'Jason', age: 37}
]);

const peopleB$ = new BehaviorSubject<Person[]>([
    {name: 'Amy', age: 40}
]);

const allPeople$ = combineLatest([peopleA$, peopleB$]).pipe(
    rxTap(console.log), 
    //=> [
    // [ {name: 'Jason', age: 37} ], 
    // [ {name: 'Amy', age: 40} ]
    // ]
    rxMap(flatten)
).subscribe(console.log);

// First Emission
//=> [
//     {name: 'Jason', age: 37},
//     {name: 'Amy', age: 40},
// ]

peopleA$.next(concat(peopleA$.value, [{name: 'James', age: 18}]));

// Second Emission
//=> [
//     {name: 'Jason', age: 37},
//     {name: 'James', age: 18},
//     {name: 'Amy', age: 40},
// ]

// And so on.

ForkJoin

Promise.all(ish) Accepts many obserservables and will only emit once all observables have completed.

import { Subject, forkJoin } from 'rxjs';
import { map as rxMap, tap as rxTap } from 'rxjs/operators';
import { concat, flatten } from 'ramda';

const peopleA$ = new Subject<Person[]>();
const peopleB$ = new Subject<Person[]>();

const allPeople$ = forkJoin([peopleA$, peopleB$]).pipe(
    rxTap(console.log), 
    //=> [
    // [ {name: 'Jason', age: 37} ], 
    // [ {name: 'Amy', age: 40} ]
    // ]
    rxMap(flatten)
).subscribe(console.log);

// No Initial Emission

peopleA$.next([{name: 'James', age: 18}]);
peopleA$.complete();

// No Emission Again

peopleB$.next([{name: 'James', age: 18}]);
peopleB$.complete();

// Only Emission
//=> [
//     {name: 'James', age: 18},
//     {name: 'James', age: 18}
// ]

RXJS Operators of Note

flatMap

import { from, of } from 'rxjs';
import { tap as rxTap, flatMap as rxFlatMap } from 'rxjs/operators';

// A function that does nothing but return the parameter supplied to it.
import { identity } from 'ramda';


const peopleObs$ = from([
    of(['Jason', 'John']),
    of(['Amy', 'Jenna'])
]);

peopleObs$.pipe(
    rxTap(console.log), 
    //=> Observable { _isScalar: false, _subscribe: [Function] }
    rxFlatMap(identity) 
    //=> [ 'Jason', 'John' ]
    //= >[ 'Amy', 'Jenna' ]
).subscribe(console.log);

switchMap

import { from, Observable, of, Subscriber } from 'rxjs';
import { switchMap as rxSwitchMap } from 'rxjs/operators';


import https, {Response} from 'https';

const peopleRequests$ = from([
    'https://swapi.co/api/people/1/',
    'https://swapi.co/api/people/2/'
]);

peopleRequests$.pipe(
    rxSwitchMap((url: string) => {
        return new Observable((obs: Subscriber<any>) => {
            https.get('https://swapi.co/api/people/', (res: Response) => {
                let rawData = '';
                res.on('data', (chunk) => { 
                    rawData += chunk; 
                });
                res.on('end', () => {
                    try {
                      const parsedData = JSON.parse(rawData);
                      obs.next(parsedData);
                      obs.complete();
                    } catch (e) {
                        obs.error(e.message);
                    }
                  });
                return obs
            }).on('error', (e) => {
                obs.error(e.message);
            });
        })
    }) 
).subscribe(console.log);

// First Emission Canceled / No Emission
// Second Emission => Successful Response

FIN

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment