The fourth dimension - An introduction to Functional Reactive Programming

The fourth dimension - An introduction to Functional Reactive Programming

In the spacetime model, the fourth dimension is time. In this talk I show how to transform and manipulate events happening in different moments in time the same way that we transform Arrays or normal sequences, by using the power of Functional Reactive Programming. This will allow us to unite synchronous and asynchronous code, in a way that will help us reason about complex code and build applications that are powerful, reliable and simple to understand.

B083b8207ccd0744a5abb18c8e75d24d?s=128

Sergi Mansilla

September 18, 2014
Tweet

Transcript

  1. The fourth dimension A practical introduction to Functional Reactive Programming

    by sergi mansilla | @sergimansilla
  2. @sergimansilla github.com/sergi

  3. Before

  4. None
  5. Shameless linkbaiting

  6. Tame your async code with this one weird trick! A

    practical introduction to Functional Reactive Programming by sergi mansilla | @sergimansilla
  7. - Linkbaiting - Time

  8. Human beings have hard-wired time in their brain

  9. Developers have hard-wired time in their brain (in a slightly

    different way)
  10. 1 CPU Cycle 1 s Level 1 cache access 3

    s Level 2 cache access 9 s Level 3 cache access 43 s Main memory access (DRAM, from CPU) 6 min Solid-state disk I/O (flash memory) 2-6 days Rotational disk I/O 1-12 months Internet: San Francisco to Australia 19 years
  11. JS developers have hard-wired async in their brain (in a

    messed up way)
  12. Callbacks Promises Generators Events

  13. We use events to deal with asynchronous tasks

  14. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  15. Why are we still micromanaging code?

  16. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  17. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  18. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  19. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  20. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  21. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2 && isAPressed) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } }); ! var isAPressed = false; document.addEventListener('keydown', e => { isAPressed = e.keyCode === 65; }, false); ! document.addEventListener('keyup', e => { isAPressed = false; }, false);
  22. We still code the how instead of the what

  23. Programming should be more about the what

  24. None
  25. Declarative

  26. Declarative

  27. document.addEventListener('keyup', e => { return isAPressed = false; }, false);

    Void callbacks
  28. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2 && isAPressed) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } }); ! var isAPressed = false; document.addEventListener('keydown', e => { isAPressed = e.keyCode === 65; }, false); ! document.addEventListener('keyup', e => { isAPressed = false; }, false);
  29. Climbers

  30. None
  31. State is dangerous

  32. Event limbo

  33. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } });
  34. Awkward error handling Memory leaks Complex (state)

  35. None
  36. Isn’t that the problem promises try to solve?

  37. var y = f(x); var z = g(y); fAsync(x).then(...); gAsync(x).then(...);

    res = stocks .filter(q => q.symbol == 'FB') .map(q => q.quote) ! res.forEach(x => ... res = stocks //async retrieval .filter(q => q.symbol == 'FB') .map(q => q.quote) ! res.subscribe(x => ... Sync Sync Promises Reactive
  38. Click! … … Click! Click!

  39. Click! , , Click! Click! [ ]

  40. None
  41. [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    .filter(n => n % 2) .map(n => 'item ' + n) .forEach(n => console.log(n)) ! // "item 1" // "item 3" // "item 5" // "item 7" // "item 9"
  42. F R P

  43. Final Resting Place

  44. Fantasy Role Playing

  45. Functional Reactive Programming

  46. None
  47. None
  48. Deal with values that change over time

  49. Alternatives: - Bacon.js - Elm Reactive Extensions for JavaScript

  50. None
  51. RxJS helps us compose asynchronous programs in a declarative way

  52. var clicks = 0; document.addEventListener('click', function register(e) { if (clicks

    < 10) { if (e.clientX > innerWidth / 2) { console.log(e.clientX, e.clientY); clicks += 1; } } else { document.removeEventListener('click', register); } }); Filter Limit to 10 Print the coordinates
  53. fromEvent(document, 'click') .filter(c => c.clientX > innerWidth / 2 })

    .take(10) .subscribe(c => console.log(c.clientX, c.clientY) })
  54. fromEvent(document, 'click') .filter(c => c.clientX > innerWidth / 2 })

    .take(10) .subscribe(c => console.log(c.clientX, c.clientY) }) Create Observable
  55. fromEvent(document, 'click') .filter(c => c.clientX > innerWidth / 2 })

    .take(10) .subscribe(c => console.log(c.clientX, c.clientY) }) Create filtered Observable
 from the first one
  56. fromEvent(document, 'click') .filter(c => c.clientX > innerWidth / 2 })

    .take(10) .subscribe(c => console.log(c.clientX, c.clientY) }) Create final Observable
 taking only first 10 results
  57. fromEvent(document, 'click') .filter(c => c.clientX > innerWidth / 2 })

    .take(10) .subscribe(c => console.log(c.clientX, c.clientY) }) Actually kick off computation
  58. Observable Observer pattern Iterator pattern

  59. Rx.Observer - OnNext() - OnError() - OnComplete()

  60. // Creates an observable sequence of 5 integers var source

    = Rx.Observable.range(1, 5) ! // Prints out each item var subscription = source.subscribe( x => { console.log('onNext: ' + x) }, e => { console.log('onError: ' + e.message) }, () => { console.log('onCompleted') }) ! // => onNext: 1 // => onNext: 2 // => onNext: 3 // => onNext: 4 // => onNext: 5 // => onCompleted
  61. var mousemove = fromEvent(document, 'mousemove'); ! var mouseCoords = mousemove.map(e

    => ({ left: e.clientX, top: e.clientY })) ! var mouseSide = mousemove.map(e => (e.clientX > window.innerWidth / 2 ? 'right' : 'left')) ! mouseCoords.subscribe(pos => coords.innerHTML = pos.top + 'px ' + pos.left + 'px') mouseSide.subscribe(s => side.innerHTML = s); Composability
  62. var emsc = Rx.DOM.jsonpRequest({ url: ‘http://seismicportal.eu/?callback=cbfunc...', jsonpCallback: 'cbfunc' }).retry(); !

    var usgs = Rx.DOM.jsonpRequest({ url: QUAKE_URL, jsonpCallback: 'eqfeed_callback' }); ! var quakes = Rx.Observable.interval(5000) .flatMap(() => emsc.merge(usgs)) .flatMap(dataset => Rx.Observable.fromArray(dataset.features)) .distinct(quake => quake.code || quake.unid) .subscribe(quake => { var coords = quake.geometry.coordinates; L.circle([coords[1], coords[0]], quake.size).addTo(map); }); Concurrency
  63. None
  64. // Search Wikipedia for a given term function searchWikipedia(term) {

    var cleanTerm = global.encodeURIComponent(term); var url = ‘/search?q=‘ + cleanTerm + '&cb=JSONPCallback'; return Rx.Observable.getJSONPRequest(url); } ! var input = document.querySelector('#searchtext'), results = document.querySelector('#results'); ! // Get all distinct key up events from the input and var keyup = fromEvent(input, 'keyup') .map(e => e.target.value) .where(text => text.length > 2) // Longer than 2 chars .throttle(200) // Pause for 200ms .distinctUntilChanged(); // Only if the value has changed
  65. var searcher = keyup .map(text => searchWikipedia(text)) // Search wikipedia

    .switchLatest() // Ensure no out of order results .where(data => (data.length === 2)); // Where we have data ! searcher.subscribe(data => { // Append the results (data[1]) }, error => { // Handle any errors });
  66. None
  67. // Search Wikipedia for a given term function searchWikipedia(term) {

    var cleanTerm = global.encodeURIComponent(term); var url = ‘/search?q=‘ + cleanTerm + '&cb=JSONPCallback'; return Rx.Observable.getJSONPRequest(url); } ! var input = document.querySelector(‘#searchtext'); var results = document.querySelector('#results'); ! // Get all distinct key up events from the input and var keyup = fromEvent(input, 'keyup') .map(e => e.target.value) .where(text => text.length > 2) // Longer than 2 chars .throttle(200) // Pause for 200ms .distinctUntilChanged(); // Only if the value has changed
  68. // Search Wikipedia for a given term function searchWikipedia(term) {

    return fromArray(['JavaScript', 'JavaServer Pages', 'JavaSoft', 'JavaScript library', 'JavaScript Object Notation', 'JavaScript engine', 'JavaScriptCore']); } ! var input = document.querySelector('#searchtext'), results = document.querySelector('#results'); ! // Get all distinct key up events from the input and var keyup = fromEvent(input, 'keyup') .map(e => e.target.value) .where(text => text.length > 2) // Longer than 2 chars .throttle(200) // Pause for 200ms .distinctUntilChanged(); // Only if the value has changed
  69. fromArray fromCallback fromEvent fromEventPattern fromIterable fromNodeCallback fromPromise

  70. var mouseup = fromEvent(dragTarget, 'mouseup'); var mousemove = fromEvent(document, 'mousemove');

    var mousedown = fromEvent(dragTarget, 'mousedown'); ! var mousedrag = mousedown.flatMap(md => { var startX = md.clientX + window.scrollX, startY = md.clientY + window.scrollY, startLeft = parseInt(md.target.style.left, 10) || 0, startTop = parseInt(md.target.style.top, 10) || 0; ! // Calculate delta with mousemove until mouseup return mousemove.map(mm => { mm.preventDefault(); ! return { left: startLeft + mm.clientX - startX, top: startTop + mm.clientY - startY }; }).takeUntil(mouseup); }); ! subscription = mousedrag.subscribe(pos => { dragTarget.style.top = pos.top + 'px'; dragTarget.style.left = pos.left + 'px'; });
  71. [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    .filter(n => n % 2) .map(n => 'item ' + n) .forEach(n => console.log(n)) ! // "item 1" // "item 3" // "item 5" // "item 7" // "item 9"
  72. [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    .filter(n => n % 2) .map(n => 'item ' + n) .forEach(n => console.log(n)) ! // "item 1" // "item 3" // "item 5" // "item 7" // "item 9" loop loop loop
  73. None
  74. fromArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    .filter(n => n % 2) .map(n => n * 100) .map(n => 'item ' + n) .subscribe(n => console.log(n))
  75. None
  76. First Class async Composability Concurrency Encapsulation Michael Hasselhof sings limbo

  77. I am writing a book about this! @sergimansilla

  78. Thanks! @sergimansilla