Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Sergi Mansilla

September 18, 2014
Tweet

More Decks by Sergi Mansilla

Other Decks in Programming

Transcript

  1. The fourth dimension
    A practical introduction to
    Functional Reactive Programming
    by sergi mansilla | @sergimansilla

    View Slide

  2. @sergimansilla
    github.com/sergi

    View Slide

  3. Before

    View Slide

  4. View Slide

  5. Shameless linkbaiting

    View Slide

  6. Tame your async code
    with this one weird trick!
    A practical introduction to
    Functional Reactive Programming
    by sergi mansilla | @sergimansilla

    View Slide

  7. - Linkbaiting
    - Time

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  11. JS developers
    have hard-wired async
    in their brain
    (in a messed up way)

    View Slide

  12. Callbacks
    Promises
    Generators
    Events

    View Slide

  13. We use events
    to deal with
    asynchronous tasks

    View Slide

  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);
    }
    });

    View Slide

  15. Why are we still
    micromanaging
    code?

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);
    }
    });

    View Slide

  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);

    View Slide

  22. We still code the how
    instead of the what

    View Slide

  23. Programming
    should be more
    about the what

    View Slide

  24. View Slide

  25. Declarative

    View Slide

  26. Declarative

    View Slide

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

    View Slide

  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);

    View Slide

  29. Climbers

    View Slide

  30. View Slide

  31. State is dangerous

    View Slide

  32. Event limbo

    View Slide

  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);
    }
    });

    View Slide

  34. Awkward error handling
    Memory leaks
    Complex (state)

    View Slide

  35. View Slide

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

    View Slide

  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

    View Slide

  38. Click!
    … …
    Click!
    Click!

    View Slide

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

    View Slide

  40. View Slide

  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"

    View Slide

  42. F
    R
    P

    View Slide

  43. Final
    Resting
    Place

    View Slide

  44. Fantasy
    Role
    Playing

    View Slide

  45. Functional
    Reactive
    Programming

    View Slide

  46. View Slide

  47. View Slide

  48. Deal with values
    that change
    over time

    View Slide

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

    View Slide

  50. View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  57. fromEvent(document, 'click')
    .filter(c => c.clientX > innerWidth / 2 })
    .take(10)
    .subscribe(c => console.log(c.clientX, c.clientY) })
    Actually kick off computation

    View Slide

  58. Observable
    Observer
    pattern
    Iterator
    pattern

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  63. View Slide

  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

    View Slide

  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
    });

    View Slide

  66. View Slide

  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

    View Slide

  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

    View Slide

  69. fromArray
    fromCallback
    fromEvent
    fromEventPattern
    fromIterable
    fromNodeCallback
    fromPromise

    View Slide

  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';
    });

    View Slide

  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"

    View Slide

  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

    View Slide

  73. View Slide

  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))

    View Slide

  75. View Slide

  76. First Class async
    Composability
    Concurrency
    Encapsulation
    Michael Hasselhof sings limbo

    View Slide

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

    View Slide

  78. Thanks!
    @sergimansilla

    View Slide