Skip to content

Instantly share code, notes, and snippets.

@pfraces
Last active November 19, 2018 23:41
Show Gist options
  • Save pfraces/e5f452704eba1301cbd015caa8753fe3 to your computer and use it in GitHub Desktop.
Save pfraces/e5f452704eba1301cbd015caa8753fe3 to your computer and use it in GitHub Desktop.
Observables (a.k.a Streams)

Functional JavaScript

Function expressions

// function statement
function greet (name) {
  console.log('Hi ' + name + '!');
}

// function expression
var greet = function (name) {
  console.log('Hi ' + name + '!');
};

greet('Bob'); // Hi Bob!

Function scope

var test = function (condition) {
  var num = 10;
  
  if (condition) {
    var num = 15;
    console.log('inner num: ' + num);
  }
  
  console.log('outer num: ' + num);
};

test(false); // outer num: 10
test(true); // inner num: 15 | outer num: 15

IIFEs (immediately invoked function expression)

var three = (function () {
  return 3;
})();

console.log(three); // 3

var five = (function (x, y) {
  return x + y;
})(2, 3);

console.log(five); // 5

preventing global scope pollution

var str = 'Hi, I am a global';

(function () {
  var secret = 'P@s5w0rd';
})();

console.log(str); // Hi, I am a global
console.log(secret); // undefined

First class functions

In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. This means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

assign function to variable

var sum = function (x, y) {
  return x + y;
};

console.log(sum(2, 3)); // 5

assign function to data structure

var math = {
  sum: sum,
  mult: function (x, y) {
    return x * y;
  }
};

console.log(math.sum(2, 3)); // 5
console.log(math.mult(2, 5)); // 10

pass function as argument

// Example 1
setTimeout(function () {
  console.log('done!');
}, 3000);

// Example 2
var doIf = function (expr, task) {
  if (expr) { task(); }
};

doIf(2 < 5, function () {
  console.log('2 is less than 5');
});

// Example 3
var doAsync = function (done) {
  connect(function () {
    read(function (data) {
      done(data);
    });
  });
};

doAsync(function (data) {
  console.log(data);
});

// Example 4
var log = function (msg) {
  console.log(msg);
};

doAsync(log);

return function from other function

var sum = function (a) {
  return function (b) {
    return a + b;
  };
};

console.log(sum(3)(4)); // 7

var sum3 = sum(3);
console.log(sum3(4)); // 7

Closures

var counter = function () {
  var count = 0;
  
  return function () {
    var curr = count;
    count = count + 1;
    return curr;
  };
};

var tick = counter();

console.log(tick()); // 0
console.log(tick()); // 1
console.log(tick()); // 2

Revealing Module Pattern

https://addyosmani.com/resources/essentialjsdesignpatterns/book/#revealingmodulepatternjavascript

var math = (function () {
  var sum = function (a, b) {
    return a + b;
  };
  
  var mult = function (a, b) {
    return a * b;
  };
  
  var square = function (a) {
    return mult(a, a);
  };
  
  return {
    sum: sum,
    mult: mult,
    square: square
  };
})();

console.log(math.square(3)); // 9

Imperative vs declarative (for vs forEach)

Iterate array (imperative)

var numbers = [ 1, 2, 3, 4 ];

for (var i = 0; i < numbers.length; i++) {
    console.log(numbers[i]);
}

Iterate array (declarative)

var numbers = [ 1, 2, 3, 4 ];

numbers.forEach(function (number) {
    console.log(number);
});

Project array (imperative)

var numbers = [ 1, 2, 3, 4 ];
var doubles = [];

for (var i = 0; i < numbers.length; i++) {
    doubles.push(numbers[i] * 2);
}

Project array (declarative)

var numbers = [ 1, 2, 3, 4 ];

var doubles = numbers.map(function (number) {
    return number * 2;
});

Function purity

Side effects

var numbers = [ 1, 2, 3, 4 ];

var double = function () {
    numbers.forEach(function (number, index) {
        numbers[index] = number * 2
    });
    
    return numbers;
};

console.log(double()); // [ 2, 4, 6, 8 ]
console.log(numbers); // [ 2, 4, 6, 8 ]

Common side effects:

  • Modifying any external variable or object property (e.g., a global variable, or a variable in the parent function scope chain)
  • Logging to the console
  • Writing to the screen
  • Writing to a file
  • Writing to the network
  • Triggering any external process
  • Calling any other functions with side-effects

Pure functions

var sum = function (a, b) {
  return a + b;
};

Immutability

var numbers = [ 1, 2, 3, 4 ];

var double = function () {
    return numbers.map(function (number) {
        return number * 2
    });
};

console.log(double()); // [ 2, 4, 6, 8 ]
console.log(numbers); // [ 1, 2, 3, 4 ]

numbers.push(5);
console.log(double()); // [ 2, 4, 6, 8, 10 ]
console.log(double([ 1, 2, 3, 4 ])); // [ 2, 4, 6, 8, 10 ]

Referential transparency

var numbers = [ 1, 2, 3, 4 ];

var double = function (singles) {
    return singles.map(function (single) {
        return single * 2;
    });
};

console.log(double(numbers)); // [ 2, 4, 6, 8 ]
console.log(numbers); // [ 1, 2, 3, 4 ]

numbers.push(5);
console.log(double(numbers)); // [ 2, 4, 6, 8, 10 ]
console.log(double([ 1, 2, 3, 4 ])) // [ 2, 4, 6, 8 ]

References

Iterator pattern

The Iterator pattern addresses the following problems:

  • The elements of an aggregate object should be accessed and traversed without exposing its representation (data structures).
  • New traversal operations should be defined for an aggregate object without changing its interface.

Defining access and traversal operations in the aggregate interface is inflexible because it commits the aggregate to particular access and traversal operations and makes it impossible to add new operations later without having to change the aggregate interface.

array methods

var numbers = [ 1, 2, 3, 4 ];

numbers.forEach(function (number) {
   console.log(numbers); // 1 2 3 4
});

var doubles = numbers.map(function (number) {
  return number * 2;
});

console.log(doubles); // [ 2, 4, 6, 8 ]

var evens = numbers.filter(function (number) {
  return number % 2 === 0;
});

console.log(evens); // [ 2, 4 ]

Other array methods:

  • reduce
  • includes
  • find
  • sort
  • every / some

object methods

var rgbs = {
  red: '#ff0000',
  green: '#00ff00',
  blue: '#0000ff'
};

// TypeError: rgbs.forEach is not a function
rgbs.forEach(function (rgb) {
  console.log(rgb);
});

Object.prototype.forEach = function (iteratee) {
  for (var key in this) {
    if (this.hasOwnProperty(key)) { 
      iteratee(this[key], key);
    }
  }
};

// #ff0000 #00ff00 #0000ff
rgbs.forEach(function (rgb) {
  console.log(rgb);
});

Object.prototype.map = function (iteratee) {
  var projection = {};
  
  this.forEach(function (value, key) {
    projection[key] = iteratee(value, key);
  });
  
  return projection;
};

var rgbToColor = function (rgb) {
  var red = rgb.slice(1, 3);
  var green = rgb.slice(3, 5);
  var blue = rgb.slice(5, 7);
  
  return {
    red: parseInt(red, 16),
    green: parseInt(green, 16),
    blue: parseInt(blue, 16)
  };
};

var colors = rgbs.map(rgbToColor);
console.log(colors); // { red: 255, green: 0, blue: 0 } ...

What about reduce, sort, ...?

library utilities

_.forEach([ 1, 2, 3, 4 ], function (item) { console.log(item); }); // lodash / underscore
R.forEach(function (item) { console.log(item); }, [ 1, 2, 3, 4 ]); // ramda

_.forEach({ foo: 123, bar: 321 }, function (item, index) {
  console.log(index + ': ' + item);
});

_.map([ 1, 2, 3, 4 ], function (item) { return item * 2; });
_.map({ foo: 123, bar: 321 }, function (item) { return item * 2 }); 

What about custom data structures?

interfaces

toString method

var person = {
  name: 'John',
  lastname: 'Doe'
};

console.log('Hi ' + person.name); // Hi John
console.log('Hi ' + person); // Hi [object Object]

person.toString = function () {
  return this.name + ' ' + this.lastname;
};

console.log('Hi ' + person); // Hi John Doe

valueOf method

var three = {
  valueOf: function () { return 3; }
};

var five = {
  valueOf: function () { return 5; }
};

console.log(three + five); // 8

var randomInt = function (max) {
  return Math.floor(Math.random() * max);
};

var randomNumber = {
  valueOf: function () { return randomInt(5); }
};

console.log(randomNumber); // Object
console.log(0 + randomNumber); // ?

iterator interface

Any object implementing the iterator interface must provide a next method which returns either { value: currentIterationValue } while iterating or { done: true } if there is no more iterations to perform

var arrayIterator = function (array) {
  var length = array.length;
  var index = 0;
  
  var next = function () {
    var value = null;
    
    if (index < length) {
      value = array[index];
      index = index + 1;
      return { value: value };
    }
    
    return { done: true }
  };

  return { next: next };
};

Iterators iteration

var forEach = function (iterator, iteratee) {
  for (var it = iterator.next(); !it.done; it = iterator.next()) {
    iteratee(it.value);
  }
};

forEach(arrayIterator([1, 2, 3, 4]), function (value) {
  console.log(value); // 1 2 3 4
});

Iterators projection

var map = function (iterator, iteratee) {
  var next = function () {
    var it = iterator.next();
    
    if (!it.done) {
      return { value: iteratee(it.value) };
    }
    
    return { done: true };
  };
  
  return { next: next };
};

var doublesIterator = map(arrayIterator([ 1, 2, 3, 4 ]), function (item) {
  return item * 2;
});

forEach(doublesIterator, function (value) {
  console.log(value); // 2 4 6 8
});

Async Iterators

TODO

Observer pattern

The Observer pattern addresses the following problems:

  • A one-to-many dependency between objects should be defined without making the objects tightly coupled.
  • It should be ensured that when one object changes state an open-ended number of dependent objects are updated automatically.
  • It should be possible that one object can notify an open-ended number of other objects.

Defining a one-to-many dependency between objects by defining one object (subject) that updates the state of dependent objects directly is inflexible because it commits (tightly couples) the subject to particular dependent objects. Tightly coupled objects are hard to implement, change, test, and reuse because they refer to and know about (how to update) many different objects with different interfaces.

https://plnkr.co/edit/8MgJGP

var pubsub = (function () {
    var listeners = [];
    
    var subscribe = function (listener) {
        listeners.push(listener);
    }

    var publish = function (msg) {
        listeners.forEach(function (listener) {
            listener(msg);
        })
    }

    return {
        subscribe: subscribe,
        publish: publish
    }
})();

var publisher = (function () {
    setInterval(function () {
        pubsub.publish(Date());
    }, 1000);
})();

var subscriberA = (function () {
    pubsub.subscribe(function (msg) {
        console.log('A:', msg);
    });
})();

var subscriberB = (function () {
    pubsub.subscribe(function (msg) {
        console.log('B:', msg);
    });
})();

Functional Programming with collections

  • function pipes
  • chaning
  • the problem with this
  • partial application

TODO

Observables (a.k.a Streams)

  • Treating events as collections
  • Manipulating sets of events with "operators"
  • Lodash for async

Callback hell

var doAsync = function (done) {
  connect(function () {
    read(function (data) {
      done(data);
    });
  });
};

Promises

myPromise.then(succesFn, errorFn);
var doAsync = function (done) {
  connect()
    .then(read)
    .then(done);
};
  • Read-only view to a single future value
  • Success and error semantics via .then
  • Not lazy. By the time you have a promise it's on its way to being resolved
  • Immutable and uncancellable. Your promise will resolve or reject, and only once

Promise creation

var myPromise = new Promise(function (resolve, reject) { ... });
var fs = require('fs');

var readPkg = new Promise(function (resolve, reject) {
  fs.readFile('package.json', 'utf8', function (err, data) {
    if (err) { reject(err); }
    else { resolve(data); }
  });
});

readPkg
  .then(function (data) {
    return JSON.parse(data);
  })
  .then(function (pkg) {
    console.log(pkg.name + ' v.' + pkg.version);
  });
});

Observables

var sub = myObservable.subscribe(valueFn, errorFn, completeFn);
sub.unsubscribe();
  • "Streams" or sets
  • Any number of things
  • Over any ammount of time
  • Lazy. Observables will not generate values until they're subscribed to
  • Can be "unsubscribed" from

Observable creation

var myObservable = new Observable(function (observer) { ... });

// Observer interface
{
  next: function (value) { ... },
  error: function (err) { ... },
  complete: function () { ... }
}
var fs = require('fs');

var readPkg = new Observable(function (observer) {
  fs.readFile('package.json', 'utf8', function (err, data) {
    if (err) { observer.error(err); }
    else {
      observer.next(data);
      observer.complete();
    }
  });
});

readPkg
  .map(function (data) {
    return JSON.parse(data);
  })
  .subscribe(function (pkg) {
    console.log(pkg.name + ' v.' + pkg.version);
  });

Cancellation

var interval = new Observable(function (observer) {
  var intervalId = setInterval(function () {
    observer.next();
  }, 500);
  
  return function () {
    cancelInterval(intervalId);
  };
});

var sub = interval.subscribe(function () {
  console.log(Date());
});

setTimeout(function () {
  sub.unsubscribe();
}, 5000);

Creation helpers

  • Observable.of(val, val2, val3, ...)
  • Observable.from(promise/iterator)
  • Observable.fromEvent(domElement,eventName)

Operators

  • Operators are methods on Observable that allow you to compose new observables
  • Operators pass each value from one operator to the next, before proceeding to the next value in the set
interval
  .map(function () { return 1; })
  .startWith(0)
  .scan(function (acc, val) { return acc + val; })
  .subscribe(function (value) {
    console.log(value);
  });

/*
|-----t--t--t--t--t-->    interval
|-----1--1--1--1--1-->    map
|--0--1--1--1--1--1-->    startWith
|--0--1--2--3--4--5-->    scan
*/

RxJS counter example

  • map()
  • filter()
  • scan()
  • take(), takeUntil()
  • startWith()
  • do()
  • merge()
  • flatMap(), switchMap()
  • combineLatest()
  • forkJoin()
  • ...

example diagrams

Hot and Cold Observables

TODO

References

RxJS

RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections.

ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events.

The Observer and the iterator design patterns are part of the twenty-three well-known "Gang of Four" design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.

References

Reactive Programming

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