Zen-like State Management with FRP



Zen-like State Management with FRP

0 0


frp-chalmers

Functional Reactive Programming presentation held at Chalmers April 2014

On Github mollerse / frp-chalmers

Zen-like State Management with FRP

Chalmers, April 2014

Stian Veum Møllersen / @mollerse

open.bekk.no

Hi, my name is Stian

I'm a frontender

Hei, jeg heter Stian. Jeg jobber i BEKK i Trondheim, som frontend utvikler. Hvorfor er jeg her i dag for å snakke om noe som høres veldig akademisk ut?

To tackle complexity

That's my job

Frontends blir bare større og større og kompleksiteten øker og øker. I web-frontend-land har vi liten tradisjon for å håndtere og takle store mengder kompleksitet.

We are taking lessons from the past

But we are forgetful

Vi prøver å lære fra fortiden, men vi går ikke så veldig langt tilbake. Det kommer stadig vekk nye spinnoffs av MV* og diverse objekt orienterte måter som lover gull og grønne skoger for kompleksitet som er på vei ut av kontroll. Vi må lenger tilbake.

Functional programming

Functional programming har sine røtter i mattematikken og har noen triks som gjør det enklere å håndtere kompleksitet i kode. Mange av dere har sikkert hørt om functional programming fra før, men vi tar en kjapp gjennomgang av hvorfor functional programming er interessant i denne sammenhengen.

Programming with functions

But wait, there's more!

Den enkleste definisjonen på funksjonell programmering er "Programming with functions", functions som byggeblokk og functions som returnerer nye functions som sement. Den er egentlig ganske dekkende. Men det er noen flere triks i baggen her, triks som er veldig nyttige for å takle kompleksitet på en måte som er behagelig for utviklere.

Immutable data

Nothing ever changes

Immutability betyr at med en gang en verdi blir instansiert vil den aldri endre seg. Dette høres egentlig mer ut som et hinder enn en fordel, tenker du kanskje? På den andre siden, ganske enkelt å resonere rundt en verdi du alltid vet vil være den samme.

Pure functions

Functions that doesn't do anything

Pure functions er immutable datas søster. En pure function er en function som ikke har noen observerbare effekter utenfor seg selv og den vil alltid returnere samme resultat gitt samme input. Hvordan skal man lage noe fornuftig hvis man ikke kan påvirke noe med funksjoner? Samtidig, ganske ålreit å forholde seg til funksjoner som alltid gir samme resultat hvis du gir den identisk input.

How does this apply to real programs?

Abstractions

For å kunne nyttegjøre seg av de gode effektene av immutability og pure functions har man i funksjonell programmering gjort noen triks. Man kan abstrahere vekk state mutation, også kjent som monader, og på den måten kunne gjøre fornuftige ting med programmering. Dette var et veldig kjapt overblikk over functional programming, det er mer to it enn dette, så jeg anbefaler å ta en nærmere kikk.

User interfaces

Så, vi ønsker å ta med oss konseptene om immutable data og pure functions til user interfaces og web fordi det hjelper oss å håndtere kompleksitet. Men, user interfaces virker som en ganske dårlig fit for det vi ønsker å få til.

Attributes of user interfaces

Global mutable state

User interfaces er en felles lekegrind for både systemet og brukeren hvor de står fritt til å forandre tilstanden til det når de føler for det. Som vi lærte i går så er det lett å gå i revefella med mutabel tilstand, spesielt når det sitter en bruker på andre siden.

Attributes of user interfaces

Asynchronous actions

All interaksjon fra brukeren, interaksjon med baksystemer. GUI tråden og whateverelse. Eventloop etc. Async og funksjonell programmering er fint mulig, men i kombinasjon med shared mutable state blir dette fort til et kaos.

Attributes of user interfaces

Temporally dependent actions

Når vi i tillegg legger til kravet om at handlinger har en streng avhengighet til tid, det vil si at rekkefølgen handlinger skjer har signifikans for utfallet av handlingene. Kombinerer vi dette med asynkronitet blir det vanskelig å unifisere det med en funksjonell tankegang.

Attributes of user interfaces

Dependencies between data

I tillegg til de overnevnte egenskapene så har vi ofte avhengigheter mellom biter av mutable state, og dette bidrar ytterligere til å skape mer shared mutable state og øke kompleksiteten.

What do we need to do in order to use functional programming principles?

  • Abstract state mutation
  • Declarative dependencies between pieces of state
  • Perserve temporal information
Vi må først abstrahere vekk state mutation slik at vi kan få trygge abstraksjoner å jobbe på. Disse abstraksjonene må også ta vare på temporal information slik at vi kan bevare temporal dependencies. Så trenger vi en deklarativ, høynivå, måte å komponere eller sy sammen disse abstraksjonene.

Reactive programming

Dette kan reactive programming hjelpe oss med å oppnå.

A programming paradigme oriented around data flows and propagation of changes to state.

Dette er den formelle definisjonen på reactive programming. Og dette høres jo akkurat ut som det vi trenger.

Data flow programming

Spreadsheets

Reactive programming omtales ofte som data flow programming. Den primære byggestenen i data flow programming, som functions er i functional programming, er datakanaler. Datakanaler representerer en bit med state og hvordan den endrer seg over tid. Det kanskje beste eksempelet på denne typen programmering er et spreadsheet.
I et spreadsheet så har du data-celler og formel-celler. Formel-cellene er avhengig av verdien i en data-celle og vil automatisk oppdatere seg hvis en av data-cellene endrer verdi.

Values and formulae

How to think in data flow

Verdier, eller kilder, og formler er de primære abstraksjonene i reactive programming. Formler i denne sammenhengen vil være tilsvarende en higher order function i functional programming, altså en reactive datatype som er avhengig av en eller flere andre reactive datatypes.

Reactive datatypes

The primary abstraction

Ved å kombinere reaktive datatyper kan vi danne oss en slags graf, hvor nodene i ytterpunktene i grafen får data satt og så kan vi propagere datene gjennom grafen og observere verdien på noder som betinger på de ytterste nodene.

How does reactive programming solve the problem with user interfaces?

Abstract state mutation

The reactive datatypes delegate all state mutation to the underlying execution model

Vi abstraherer vekk state mutation ved å la den underliggende mekanikken ta seg av det. Dette gir oss en datatype som alltid vil være oppdatert med sin seneste tilstand uten at vi som programmerere trenger å sjekke manuellt.

Declarative dependencies between pieces of state

Higher order reactive datatypes

Vi kan lage higher order reactive datatypes, datatypes som har avhengigheter til andre datatypes.

Perserve temporal information

Denne er kanskje den minst åpenbare av de tre problemene og den krever at det taes noen hensyn i implementasjonen av de reaktive datatypene.

Perserve temporal information

Save the time information connected to a change in state

Ved å få den underliggende implementasjonen ta vare på tidspunkter i forbindelse med tilstandsendringer kan vi sørge for å bevare de temporale avhengighetene.

Continous and discrete

f(t) => v og [(v, t)]

Når vi tar vare på tidsinformasjonen så kan vi representere to ulike tidsfenomener. Kontinuerlig, som vil si at ved en hver tid t så har vi en verdi. Og diskret, som vil si at vi har en liste av tid-verdi-par.

Functional & Reactive

Så, hvordan blir det når vi slår sammen de to programmerings paradigmene functional og reactive?

EventStream & Behavior

Primary abstractions for discrete and continous values

De to primære abstraksjonene vi jobber med blir EventStreams, for diskrete tidsfenomener, og Behaviors, for kontinuerlige tidsfenomener. Hva er forskjellen mellom de to? Egentlig bare semantikk, du kan gjøre en EventStream til en Behavior ved å la den huske sin siste verdi. Det er ikke alle FRP implementasjoner som behandler dem som to ulike abstraksjoner heller.

Combinators and functors

Compositional vocabulary from functional programming

Måten vi komponerer de reaktive datatypene tar vi fra funksjonell programmering. Vi bruker våre gode venner map, reduce og filter og kan på denne måten uttrykke forhold mellom datatyper og sette opp avhengighetsgrafen vår.

We are still mutating the state at the borders of the graph.

Dette er litt tilbake til det jeg sa helt i starten om functional programming. Vi kan aldri bli helt fri fra mutable state, et sted må vi gjøre det. Men vi kan begrense oss til et minimum.

What do we achieve?

Functional reasoning despite unfit domain

Det vi oppnår er å kunne benytte oss av immutability og pure functions som gjør det enklere å resonnere rundt koden din innenfor dependency grafen vi oppretter ved å komponere de reaktive datatypene funksjonellt.

FRP in practice: Implementations

Det har kanskje vært i overkant teoretisk så langt, så la oss se litt på det praktiske.

Implementations of FRP

Haskell and friends

Det kommer kanskje ikke som en overaskelse at de første implementasjonene av functional reactive programming ble gjort i Haskell. Det som kanskje overasker litt er at det var i 1997. FRP er også implementert i andre funksjonelle språk som feks Clojure.

Implementations of FRP

Web and JavaScript

  • RxJS
  • Elm
  • Flapjax
  • Bacon.js
Selv om Haskell er veldig interessant i seg selv så er det ikke det språket som vi bruker mest i hverdagen. Som web-utvikler er jeg interessert i noe som fungerer på webben. RxJS er en port av Reactive Extensions fra Microsoft og benytter seg av en LINQ-like syntax for komposisjon. Elm er et eget språk som kompilerer ned til web. Flapjax er en av de eldste FRP implementasjonene i JS, men er ikke like mye brukt. Bacon.js er et nyere, men mye mer aktivt brukt FRP lib og det vi kommer til å ta utgangspunkt i for resten av foredraget.

FRP in practice: Bacon.js

Bacon.js er implementert i JavaScript og er ganske unobtrusive å ta i bruk. Bibloteket holder seg tro til functional reactive programming tanken om en dependency graf hvor du på innsiden holder deg til immutable data og pure functions.

Sources

  • fromEventTarget
  • fromPromise
  • constant
  • and more
Bacon.js har en del metoder for å wrappe data eller kilder til data i en reaktiv datatype som kan benyttes i FRP grafen. fromEventTarget kan wrappe noe som følger eventEmitter-interfacen. fromPromise kan wrappe et promise. constant lager en behavior, en kontinuerlig verdi, som allitd har den samme verdien. Vi kan også konvertere mellom EventStreams og Behaviors, som i Bacon.js kalles properties etter behov.

Sinks

onValue

Vi trenger også en måte å få data ut av dependency grafen vår. Ved å attache en onValue funksjon til en datakilde kan vi gjøre noe med den wrapped verdien. Denne fungerer både på properties og eventstreams.

Composition

  • map
  • filter
  • reduce
  • merge
  • and many more
Vi kan komponere disse datatypene og lage mer avanserte strukturer og oppførsel. Bacon.js har veldir mange fler kombinasjonsmetoder enn dette, men disse er de mest brukte.

Low level

Only the important bits

Bacon.js gir oss bare de byggestenene vi må ha og oppfordrer dermed, i god functional programming ånd, programmereren til å bygge de abstraksjonene som eventuelt mangler av de bitene Bacon.js tilbyr.

Example: One-directional binding

Det er fortsatt litt abstrakt, så la oss ta et par eksempler for å runde av foredraget. Det første eksempelet er en veldig enkel one-directional binding.

Goal: Text in inputfield is replicated in label

var input = Bacon.fromEventTarget(document.querySelector("#text", 'keyup')
Vi starter med å wrappe verdien i tekstfeltet i en reaktiv datatype. Vi angir også hvilken event vi ønsker at skal propagere nye verdier.
var value = input.map(el => el.value)
Vi wrapped et DOM element, men det vi egentlig er interessert i er verdien til input feltet, så da kan vi lage oss en ny node i grafen som representerer den verdien ved å bruke map.
value.onValue(v => document.querySelector('label').innerHTML = v);
Så kan vi unwrappe verdien igjen ved å benytte onValue og sette den verdien på labelen. Dette eksempelet er veldig simpelt og det virker kanskje litt overkill å benytte Bacon.js til dette, men det illustrerer hvordan sources og sinks funker på en veldig fin måte.

Demo

Example: WebSockets and multiple clients.

We are making a simple voting system based on WebSockets.

What we need to do

  • Connect and receive data from a WebSocket.
  • Count the votes.
  • Calculate the percentage each alternative gets.
  • Reflect the percentage in the interface.
var socketStream = Bacon.fromEventTarget(socket, "vote");

Her er socket.io brukt for å håndtere WebSockets, og ettersom det er en implementasjon av EventEmitter, kan vi bruke det i fromEventTarget.

var totalVoteProperty = function (id) {
  return socketStream
    .filter(isId(id))
    .map(1)
    .scan(0, add);
};

Vi skal finne totale antall stemmer et alternativ får.

Med å kjøre filter med predikatet isId, får vi ut en hendelse som kun gjelder for alternativet med gitt ID.

Vi gjør om hendelsen til å kun ha verdier som er 1. Det vil si at hver gang hendelsen inntreffer, vil vi få verdien 1.

Scan som er nesten som reduce, bare at den gir melding hver gang den oppdateres. Reduce ville kun ha gitt en verdi når det ikke kommer flere verdier (WebSocketen hadde avsluttet). Når vi bruker scan her, og gir en initiell verdi på 0 og _add som transformator, vil resultatet være den totale summen.

Vi sitter igjen med en adferd som inneholder summen av stemmer for et alternativ med en gitt ID.

var percentage = function (numVotes, total) {
  return numVotes
    .combine(total, _toPercentage);
};

Ettersom vi skal vise prosentvis fordeling av alle alternativene, må ha en måte å regne det ut på.

Vi starter med antallet stemmer til et alternativ.

Combine vil kombinere de to siste verdiene på begge kildene ved hjelp av en første ordens funksjon.

Om vi kombinerer antall stemmer på et alternativ med totale mengden stemmer og kjører prosentregning på den, har vi prosenten på antall stemmer til et gitt alternativ.

Hei folkens, husk valget i dag om 2 timer! Alle med aktivt oblat kan og bør stemme.

Det vi sitter igjen med etter dette er en adferd med prosenten til et alterantiv.

var sum = socketStream.map(1).scan(0, _add);

For å finne den totale mengden stemmer, kan vi gjøre det på samme måten som vi har gjort på individuelle alternativer, bare uten filtreringen.

["alt1", "alt2", "alt3"].forEach(function (id) {
  return percentage(totalVoteProperty(id), sum)
  .onValue(v => document.querySelector('#'+id).value = v);
});

For å samle det hele, begynner vi med å iterere vi over ID-ene til de tre alternativene.

Vi ønsker å finne prosenten av den totale stemme-antallet for alternativ "ID" ut i fra den totale summen.

Vi vil sette denne prosenten til på DOM-elementet gitt av ID.

Det er det som trengs. Og vi har nå et fiks ferdig sanntids polling-system!

How do you spell banana in swedish?

Vote at: http://frp.herokuapp.com

Wrap up

Managing complexity is hard

But functional programming makes it easier

Det er vanskelig å håndtere kompleksitet, men functional programming har et par triks til oss for å gjøre det lettere i form av immutable data og pure functions.

User interfaces are tricky

But reactive programming make them manageable

User interfaces har en del karakteristikker som gjør dem vanskeligere å ha med å gjøre i en funksjonell setting. Ved å benytte oss av det reactive programming gir oss i form av state management kan vi gjøre dem enklere å håndtere.

Combinine functional and reactive programming for a plesant way of managing complexity in user interfaces

Moralen i historien er;

Questions, feedback and other things

@mollerse

stian.veum.mollersen@bekk.no

Thank you for having me!