Functional programming – Teil 4

Weichenpfeil

Functional programming – Teil 4

Bisher hatten wir reichlich Theorie und haben noch einiges mehr, was wir uns ansehen sollten. Aber vielleicht schauen wir mal, was wir mit dem, was wir bisher so gelernt haben auf die Beine stellen können.

Machen wir uns die WordPress REST-Api zu Nutze und bauen eine kleine App, welche die letzten Posts abholt und in eine Liste transformiert.

Ablaufplan

  1. Url für den posts Endpunkt erzeugen.
  2. Den API-Aufruf durchführen.
  3. Die JSON Response in LI Tags umwandeln.
  4. Die LI Tags in die DOM klemmen.

Für compose und map werden wir ramda einsetzen. JQuery darf sich dann um den Ajax Request und die DOM-Manipulation kümmern.

Um los legen zu können benötigen wir also ein HTML Dokument mit den entsprechenden script tags. Das JavaScript können wir direkt inline schreiben.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <!-- APP GOES HERE -->
    <script>
    </script>
  </body>
</html>  

Dann mal los

Kümmern wir uns erstmal um das Einfachste, die Url für den Endpunkt erzeugen.

const {compose, map} = R // R is the global ramda namespace

// pure
const baseUrl  = "https://www.avanzu.de/wp-json/wp/v2"
const postsUrl = _ => `${url}/posts` 

Hier müssen wir an sich nur darauf achten, dass postsUrl eine Funktion ist, damit wir damit composition betreiben können.

ES6 sieht für Funktionen ohne Parameter eigentlich leere runde Klammern vor. Es kommt jedoch ziemlich häufig vor, dass der Unterstrich für Parameter eingesetzt wird, die aufgrund der Signatur akzeptiert werden müssen aber eigentlich belanglos sind.

Da fetch ein Response Objekt liefert dem wir das JSON erst noch abringen müssen, haben wir hier noch die toJson Funktion. Zugegeben, für den Anwendungsfall eher unnötig aber übersichtlich.

Die ersten Unreinheiten

Damit wir sehen womit wir arbeiten, verschaffen wir uns erstmal einen Überblick über die Daten, die uns der API Aufruf liefert. Dafür brauchen wir zwei Funktionen, die Nebeneffekte produzieren.

// impure
const fetchJson = fn => url => $.getJSON(url).then(fn)
const inspect   = label => x => (console.log(label, x), x)

Gut, fetchJson macht den eigentlichen Ajax Aufruf, und übergibt dann das JSON an eine beliebige Funktion. Für den ersten Überblick wird das inspect sein.

Die Funktion inspect verwendet übrigens einen weiteren Trick um sich das implizite return zu Nutze machen zu können: Was in runden Klammern steht wird als einzelner Ausdruck ausgewertet. Dieser Ausdruck wird von links nach rechts ausgewertet. Der Letzte Wert in dieser Aufzählung (hier x) ist dann das Ergebnis dieses Ausdrucks.

Die erste Composition des Abends

// compose app
const executeApp = compose(fetchJson(inspect('Response')), postsUrl)
// run app
executeApp()

Bauen wir uns erstmal die Url zusammen und übergeben diese an fetchJson welche inspect als callback verwendet.
Ein Blick in die Console verrät uns, wo in der Datenstruktur der Titel des Post versteckt ist.

Alles klar, wir benötigen für unseren Fall den Pfad title.rendered. Bauen wir uns für den Zugriff ein paar Funktionen.

Daten extrahieren

// pure 
// ... 
const prop     = s => obj => obj[s]
const theTitle = compose(prop('rendered'), prop('title'))

Mit prop können wir den Zugriff auf ein beliebiges Attribut abbilden. Wir müssen nur definieren, welches Attribut wir denn gerne hätten.
Durch composition können wir dann bereits den gewünschten Pfad abbilden.

Mal sehen ob das auch so funktioniert, wie wir uns das vorstellen.

const render     = compose(inspect('titles'), map(theTitle))
const executeApp = compose(fetchJson(render), postsUrl)

Damit uns der callback für fetchJson nicht aus dem Ruder läuft habe das ganze in eine render Funktion ausgelagert. Ab jetzt können wir uns voll und ganz auf die render Funktion konzentrieren.

Achtung, map

Hier müssen wir berücksichtigen, dass render mit einem Array beliefert wird.

Da theTitle jedoch für genau ein Objekt funktioniert müssen wir die Funktion map einsetzen.

Diese macht eigentlich nichts anderes als den callback aufzunehmen. Sobald sie ihren Functor bekommt, wird dessen map Funktion mit dem callback aufgerufen.

const map = fn => xs => xs.map(fn)

Führen wir den aktuellen Stand eimal aus.

// console
"titles" [
"Functional programming – Teil 3", 
"Functional programming – Teil 2", 
"Functional programming – Grundlagen", 
]

Daten präsentieren

Ausgezeichnet, genau die Rohdaten, die wir brauchen. Sorgen wir nun dafür, dass sie in den Browser kommen.

<body>
  <div class="widget">
    <h2>Functional list</h2>
    <div id="post-container"></div> 
  </div>
</body>

Den statischen Teil hätten wir. Kümmern wir uns darum, die Rohdaten in LI Tags zu transformieren. Diese packen wir dann in eine UL.

// pure 
// ... 
const li   = s  => `<li>${s}</li>`
const ul   = s  => `<ul>${s}</ul>`
const join = xs => xs.join('')

Das müsste ausreichen um am Ende mit einer UL da zu stehen. Setzen wir es mal in die render composition ein.

const render = compose(inspect('The UL'), ul, join, map(li), map(theTitle))

Der Transformationsfluss müsste mittlerweile einigermaßen gut zu verfolgen sein. Rekapitulieren wir aber trotzdem noch mal.

  • Wir bekommen ein Array mit posts.
  • Daraus extrahieren wir für jedes Element den title.rendered Pfad.
  • Um jedes dieser Elemente legen wir einen LI Tag.
  • Das Array aus LI Tags wird zu einem String zusammengefasst.
  • Dieser String wird in einen UL Tag platziert.
  • Der wird wiederum an unsere inspect Funktion übergeben.

Ziemlich Ausdrucksstark, finde ich. Funktioniert’s denn auch?

// console
"The UL" "<ul><li>Functional programming – Teil 3</li><li>Functional programming – Teil 2</li><li>Functional programming – Grundlagen</li></ul>"

Sieht gut aus, würde ich sagen. Dann müssen wir ja nur noch dafür sorgen, dass die UL auch endlich in die DOM eingefügt wird.

// impure
// ... 
const setHtml = el => html => $(el).html(html)

Auch das müsste mittlerweile ein vertrauter Anblick sein. Wir haben eine JQuery Methode in eine curried Funktion platziert und die Argumente in eine für uns günstige Reihenfolge gebracht.

// ...
const render = compose(setHtml('#post-container'), ul, join, map(li), map(theTitle))

Mission erfüllt

Führen wir das ganze noch mal aus werden wir auch endlich mit unserer Liste begrüsst.

Refactoring

Schauen wir uns die render Funktion noch mal etwas genauer an. Wir haben an einer Stelle zwei mal map direkt nebeneinander. Wir laufen also zwei mal über die Elemente des Array. Das können wir optimieren dank einer Gesetzmässigkeit bezüglich map und compose.

compose(map(f), map(g)) === map(compose(f, g))

Fassen wir also unsere beiden Array Transformationen zusammen um jedes Element in einem Schwung in die richtige Form zu bringen.

const render     = compose(setHtml('#post-container'),ul,join, map(compose(li, theTitle)))

Bonus

Damit die Liste nicht ganz so trostlos aussieht sollten wir vielleicht noch ein wenig styling betreiben. Und, da wir es mit einem Ajax-Call zu tun haben Wäre ein progress bar vermutlich auch keine schlechte Idee.

See the Pen App by composition by Marc Bach (@avanzu) on CodePen.0


 

Keine Kommentare

Deinen Kommentar hinzufügen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.