On Github mollerse / frp-intro
BEKK Fagdag
Stian Veum Møllersen / @mollerse
Mikael Brevik / @mikaelbrevik
Kombinasjonen mellom to paradigmer:
Funksjonell programmering Reaktiv programmeringDe fleste her kjenner sikkert til funksjonell programmering av en eller annen grad, men vi skal gå kjapt igjennom hva det er.
Handler om hvilken input et system får, i motsetning til hvilken tilstand systemet har. Gitt at input alltid er lik, vil også output bli like - altså rene (pure) funksjoner.
Ettersom funksjonell programmering skal hindre bieffekter, opererer det gjerne med immutable objekter (objekter/verdier som ikke kan endres).
Deklarativ programmering opererer som regel med utrykk (expressions) som kan evalueres til verdier, fremfor erklæringer (statements) som forklarer fremgangsmåte.
Markup som HTML kan sees som deklarativt, ettersom vi definerer hva vi vil ha, ikke hvordan vi skal ha det.
Eksempel: I imperativ programmering ville vi ha iterert over verdier med en for-løkke og endret verdiene i løkke-kroppen. I deklarativ programmering vil vi typisk sende med en første-klasses høyere ordens funksjon (funksjoner som kan sendes som argument til andre funksjoner - høyere ordens funksjoner: funksjoner som kan returnere andre funksjoner) som kjøres rekursivt (head | rest...) over en sekvens og returnerer en ny sekvens med det transformerte innholdet.
Der et funksjonskall påvirker systemet på annet måte enn kun det funksjonskallet returnerer.
F.eks endringer på statiske/globale variabler i en funksjon. Dette påvirker systemet/tilstanden til systemet.
Map tar inn et en sekvens og en høyere ordens funksjon, returnerer en ny sekvens der alle elementer i sekvensen er blitt kjørt via innsendt funksjon.
Reduce kjører rekursivt over en sekvens og evaluerer en høyere ordens funksjon som tar inn resultatet fra forrige kjøring og gjeldene element i sekvensen. Ved første iterasjon vil verdien fra første kjøring være satt til en initiell verdi.
Filter vil og kjøres rekursivt og ta inn en sekvens og et predikat (predikat kan forklares som noe som returnerer true/false basert på argumenter) og returnerer en ny sekvens kun med de elementene som oppfyller predikatet.
Ikke alle språkene er 100% funksjonelle, da de tillater mutable objekter og bieffekter, men kan brukes funksjonelt og har støtte for første-klasses høyere-ordens funksjoner.
Kalles ofte dataflyt (dataflow) programmering.
Gitt at du har variabeler med avhengigheter (f.eks c = a * b), dersom A endres, vil endringene propagere i dataflyt grafen og oppdatere de verdiene som er avhengig av variabel A (i dette tilfellet verdien C).
Det kan være flere nivå av avhengigheter og komplekse dataflyt grafer.
I Excel, om en har 5 verdier i kolonne A, og en celle som har SUM(A1:A5), vil summen oppdateres dersom en av cellene i kolonne A blir endret.
Dette er enkle eksempler på reaktivitet, men det kan også brukes i større komplekse sammenhenger, særlig med brukerinteraksjoner og animasjoner.
Så. Da har vi funksjonell programmering og vi har reaktiv programmering. Nå skal vi se på hvordan dette blir når vi slår det sammen.
Adferder og hendelser er to forskjellige kilder av informasjon som vi skal se på litt nærmere.
En kontinuerlig verdi. Eksemeplvis klokken, høyde eller vektorgrafikk.
En adferd vil alltid ha en verdi. Det er noe som er kontinuerlig, og som er målbart.
Selv om en adferd har en kontinuerlig verdi kan den måles/observeres til å ha en spesifikk verdi.
F.eks selv om høyden min aldri ikke eksisterer (altså er kontinuerlig), kan jeg måle den på nåværende tidspunkt.
En diskret verdi. Eksempelvis muse-klikk eller en mengde.
Diskrete verdier eksisterer kun når de inntreffer, og i mellomtiden er det ikke eksisterende.
F.eks om en gjør et museklikk vil den ha en verdi (binært 1, i dette tilfellet) i det musen blir klikket, men i det den er ferdig-trykket, vil verdien slutte å eksistere.
F.eks med bruken av "map", "reduce" eller "filter".
Om man bruker "map" på en hendelse, vil et funksjonskall bli kalt på verdien hver gang hendelsen inntreffer. Utfallet etter map vil være en ny hendelse der verdien er transformert.
Om en f.eks bruker "filter", vil en få ut en hendelsessekvens som kun inneholder hendelser som oppfyller predikatet som er sendt inn i filter-kallet.
Med "reduce" vil det som bli returnert være en adferd, ettersom den da alltid vil ha verdier.
Representasjon av tilstand
Siden GUIer er naturlig tilstandsfulle trenger vi noe som kan representere den tilstanden og tillate oss å endre den. Reaktiv programmering gir oss datatyper for å representere tilstanden elementene i GUIet vårt. Til forskjell fra det velkjente MVC-konseptet så dreier representasjon i FRP seg mer om å dele ting opp i enkeltelementer kontra sammensatte kompoenenter. Den asynkrone naturen til et brukergrensesnitt er også spesielt utfordrende å forholde seg til hvis man samtidig skal sjonglere tilstand. Fordelen med reaktive datatyper i GUI design er at vi får automatisk propagering av tilstandsendringer og ikke behøver å håndtere dette selv.Mange bekker små...
For å lage sammensatt data i FRP kan vi kombinere de reaktive datatypene slik at de danner mer komplekse strukturer. Den store fordelen her er at du til en hver tid er garantert av den reaktive egenskapen at endringer i tilstand propagerer gjennom hele den sammensatte strukturen. Her skiller FRP seg mest fra objekt-orienterte implementasjoner. Den primære funksjonen til et objekt i objekt-orientert kode er å kunne abstrahere vekk intern tilstand og varsle eksternt om relevante endringer i intern tilstand. Det blir da opp til hver enkelt komponent som er interessert i endringene i modellen å gjøre noe med det.Implementasjon og konsept
Den vanligste måten å realisere model-konseptet fra MVC med objekt orientering. I vanlig objekt-orientert kode er det vanlig å innkapsle tilstand i objekter og så tilby metoder som eksponerer eller endrer den interne tilstanden. Sammenligner vi det med de reaktive datatypene fra FRP, så blir de mer som enkelte attributter på et model-objekt. Og komposisjoner av disse datatypene vil gi den samme effekten av sammensatt data som i objekt-orientert model. Siden MVC-Model egentlig ikke legger noen føringer på implementasjonsdetaljer så er det ingenting i veien for å realisere konseptet som sammensatte reaktive datatyper. Da blir MVC-Modelen også en reaktiv datatype. Og man oppnår den samme avgrensningen av funksjonalitet, dog uten innkapslingen.Selvfølgelig, reaktive datatyper fikser biffen
Purely-functional tillater ikke side-effects. Men, siden vi benytter oss av reaktiv programmering så vil de reaktive data-typene håndtere side-effectene for oss slik at vi kan forholde oss til en funksjonell verden uten at side-effektene påvirker oss. Dette gjør oss i stand til å benytte funksjonell programmering til å komponere de reaktive datatypene og transformere data.Med generelle kombinatorer
For å skape mer avansert oppførsel i grensesnittet kan man i FRP komponere de reaktive datatype funksjonelt. Fordelen med at det er så få bestandeler i FRP er at du får et felles grensesnitt mellom komponentene du benytter deg av. Du kan dermed bruke generelle kombinatorer som map, filter og reduce. Dette gjør det blir enklere å sette sammen enkle komponenter som sammen blir til mer en kompleks komponent.Keeping it DRY
At du hele tiden benytter deg av de reaktive datatypene som ikke har noen intern tilstand i en hendelse gjør at du har mye friere tøyler når det kommer til gjenbruk av funksjonalitet. Du slipper å strukturere gjenbruk som et resultat av forhold mellom objekter, typiske is-a eller has-a forhold.Binde adferder til GUI elementer
Den vanligste måten å synliggjøre data i grensesnitt med FRP er å tilordne en GUI komponent en avhengighet til en adferd. Dette forholdet er reaktivt, på lik linje med resten av komponentene i FRP, og sørger dermed for at GUI komponenten alltid reflekterer tilstanden til det elementet det har en avhengighet til. Det beste med denne måten å gjøre data synlig i grensesnittet er at det hele er deklarativt. Til forskjell fra den tradisjonelle hendelses-drevne måten å gjøre ting på, hvor alt er manuelt. Så setter man heller opp rettede avhengigheter mellom elementer og komponenter og konsturerer et slags nettverk mellom data-kilder og data-konsumenter.FRP eller ikke?
Mange kjenner sikkert til Knockout.js og synes det deklarative bindingskonseptet virker ganske likt det som blir skissert i FRP. I Knockout.js har de konseptet om Observables. Observables varsler sine avhengigheter om endringer i tilstand, på samme måte som de reaktive datatypene i FRP. I Knockout dreier det hele seg om deklarative bindinger i templates som automatisk er bundet mot observables i bakenforliggende ViewModels og er sånt sett tro mot konseptet FRP. En av de stedene Knockout.js og FRP skiller litt lag er i den funksjonelle biten. FRP legger til rette for funksjonell komposisjon for å oppnå avansert oppførsel. Knockout har en mye mer begrenset form for komposisjon som baserer seg på semantikk fra objekt orientering. Reaktivt? Ja. Funksjonellt? Nei.Oppsummering
Enter Bacon.js
Bacon.js er et bibliotek for Javascript som har et fabelaktig navn.
Det er litt annen terminologi i Bacon.js.
fromEventTarget baserer seg på "on"-funksjon. Det vil si vi kan ta inn f.eks EventEmitter API-et eller jQuery events.
fromPromise tar inn et objekt fra Promise API-et, f.eks jqHXR (jQuery Ajax kall) eller Node Promises.
fromCallback lager en eventstream fra et callback – kan også brukes i Node.js
En kan også innkapsle f.eks konstanter eller lister, eller f.eks basert på intervall.
I knockout.js er deklarativiteten i markup, og ikke i javascripten.
Bacon.js vil altså føre til at javascripten også blir deklarativ.
var record = Bacon.fromPromise($.get('/record'));
record.map('.name') .assign($("#name"), 'val');
record.map('.rank') .assign($("#rank"), 'val');
record.map('.serialnumber') .assign($("#serialnumber"), 'val');
record.onValue(function() { $("input").trigger('keyup'); });Fordi DOMen ikke er 100% reaktiv, som bacon.js, må vi tvinge DOMen til å propagere tilstandsendringen videre.
var propertyFromInput = function(field) {
return Bacon.fromEventTarget(field, 'keyup')
.map(value)
.toProperty(value());
}
var name = propertyFromInput($("#name"));
var rank = propertyFromInput($("#rank"));
var serialnumber = propertyFromInput($("#serialnumber"));
var newRecord = Bacon.combineTemplate({ "name": name, "rank": rank, "serialnumber": serialnumber });
var validName = name.map(Boolean);
validName.not().assign($("#name + span"), 'toggle');
var validRank = rank.map(function(r) { var validRanks = ['Captain', 'Corporal', 'General', 'Private']; return validRanks.indexOf(r) != -1; });
validRank.not().assign($("#rank + span"), 'toggle');
var validSerialnumber = serialnumber.map(function(s) { return /^\d{5}-\w{3}$/.test(s); });
validSerialnumber.not() .assign($("#serialnumber + span"), 'toggle');
validName.and(validRank).and(validSerialnumber).not() .assign($("button"), 'attr', 'disabled');
var save = Bacon.fromEventTarget($("button"), 'click') .doAction('.preventDefault');
var response = newRecord.sampledBy(save)
.flatMapLatest(function(record) {
return Bacon.fromPromise($.ajax({ "url": "/record", "type": "POST", "data": JSON.stringify(record) }));
});
var socketStream = Bacon.fromEventTarget(socket, "vote")
Her er socket.io brukt for å håndtere WebSockets, og ettersom det har et API-lignende 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.
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)
.assign($("#" + id), "val");
});
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!