Introduksjon til Functional Reactive Programming (FRP) – Funksjonell programmering – Reaktiv programmering



Introduksjon til Functional Reactive Programming (FRP) – Funksjonell programmering – Reaktiv programmering

0 0


frp-intro

Intro til FRP - Fagdag presentasjon

On Github mollerse / frp-intro

Introduksjon til Functional Reactive Programming (FRP)

BEKK Fagdag

Stian Veum Møllersen / @mollerse

Mikael Brevik / @mikaelbrevik

open.bekk.no

Agenda

Teoretisk introduksjon FRP i front-end Bacon.js Praktiske eksempler og demo

Hva er Functional Reactive Programming?

Kombinasjonen mellom to paradigmer:

Funksjonell programmering Reaktiv programmering

Funksjonell programmering

De fleste her kjenner sikkert til funksjonell programmering av en eller annen grad, men vi skal gå kjapt igjennom hva det er.

En deklarativ programmeringsparadigme med funksjoner i fokus.

Unngår tilstander og mutable objekter.

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 forklarer hva som skjer.

Imperativ programmering forklarer hvordan det skal skje!

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.

Unngår uønskede bieffekter

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.

Vanlig med bruk av blant annet map, reduce og filter.

Eksempler på språk kan være Lisp, Haskell, Scala, men også JavaScript

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.

Reaktiv programmering

Kan beskrives som en metode for å holde systemet kontinuerlig oppdatert med omgivelsene.

Kalles ofte dataflyt (dataflow) programmering.

En måte for data å reagere på endringer og oppdatere for å reflektere disse endringene.

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.

Et eksempel på reaktiv data kan være et regneark og summen av to tall.

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.

Funksjonell Reaktiv Programmering

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.

To generelle konsepter

Behaviours (Adferd) Events (Hendelse)

Adferder og hendelser er to forskjellige kilder av informasjon som vi skal se på litt nærmere.

Adferder

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.

Hendelser

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.

I FRP blir adferder og hendelser behandlet som sekvenser og kan bli håndtert på en funksjonell måte.

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.

FRP i frontend

Reaktive datatyper

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.

Sammensatt data

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.

Forskjell fra MVC-Model

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.

GUI uten bi-effekter?

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.

Funksjonell komposisjon

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.

Gjenbruk av kode

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.

Synlighet i grensesnittet

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.

Knockout.js

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.

FRP i frontend

Oppsummering

  • Reaktive datatyper
  • Funksjonell komponering
  • Representerer GUI-komponenter med reaktive datatyper
Du har to reaktive datatyper; hendelser og adferder. Avansert funksjonalitet oppnåes ved å kombinere adferder og hendelser i større og mer komplekse strukturer med funksjonell komposisjon. Komponenter i GUIet representeres som en slik reaktiv datatype og får dermed dra nytte av de reaktive egenskapene med automatisk propagering av tilstandsendringer.

Praktisk FRP

Enter Bacon.js

Bacon.js er et bibliotek for Javascript som har et fabelaktig navn.

I Bacon.js har adferd og hendelser andre navn.

En adferd kalles en Property og hendelse en EventStream.

Det er litt annen terminologi i Bacon.js.

I Bacon.js innkapsler vi datakilder som reaktive datatyper.

  • fromEventTarget
  • fromPromise
  • fromCallback
  • ...med flere

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.

Alt i Bacon.js er ren javascript.

I knockout.js er deklarativiteten i markup, og ikke i javascripten.

Bacon.js vil altså føre til at javascripten også blir deklarativ.

Eksempel: Ufylling av skjema

Funksjonalitet i eksempelet

  • Fylle skjema med serverdata
  • Validere enkelt-inputs
  • Validere hele skjemaet
  • Sende tilbake til server

Fylle skjemaet med serverdata

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.

Validere enkelt-inputs

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

Validering av hele skjemaet

validName.and(validRank).and(validSerialnumber).not()
    .assign($("button"), 'attr', 'disabled');

Sende data tilbake til server

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

Demo

Name
!
Rank
!
Serial Number
!
Add

Eksempel med WebSockets og flere klienter.

Vi skal lage et enkelt system for spørreundersøkelser basert på WebSockets.

Vi må gjøre følgende

  • Koble til og hente verdier ut fra WebSockets.
  • Dersom vi får gyldig verdi fra WS, summere opp stemmene.
  • Bruke totale stemmer og indeviduelle stemmer til å regne ut prosent.
  • Representere resultatet i grensesnittet.
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!

Demo

Hva er din politiske hjertesak?

Avgi din stemme på: http://frp.herokuapp.com

Takk for oss!

Spørsmål?