Skip to content

Kapitel 08: Tupperware

Der mächtige Container

http://blog.dwinegar.com/2011/06/another-jar.html

Wie wir bereits gesehen haben, können Programme durch Verkettung reiner Funktionen Daten verarbeiten. Dies sind deklarative Verhaltensspezifikationen. Doch wie verhält es sich mit Kontrollfluss, Fehlerbehandlung, asynchronen Aktionen, Zuständen und – wage ich zu sagen – Effekten? In diesem Kapitel werden wir das Fundament entdecken, auf dem all diese nützlichen Abstraktionen aufbauen.

Zuerst erstellen wir einen Container. Dieser Container muss Werte beliebiger Typen aufnehmen können – ein Ziplock-Beutel, der nur Tapiokapudding enthält, ist kaum nützlich. Es wird ein Objekt sein, aber wir vergeben keine Eigenschaften und Methoden im OO-Sinn. Vielmehr behandeln wir es wie einen Schatz – eine spezielle Box, die unsere wertvollen Daten bewahrt.

js
class Container {
  constructor(x) {
    this.$value = x;
  }
  
  static of(x) {
    return new Container(x);
  }
}

Hier ist unser erster Container. Wir nannten ihn »Container«. Mit »Container.of« als Konstruktor sparen wir uns das lästige »new«-Schlüsselwort. Die »of«-Funktion birgt mehr Tiefe, aber vorerst betrachten wir sie als den korrekten Weg, Werte in unseren Container zu platzieren.

Untersuchen wir unsere neu geschaffene Container-Struktur...

js
Container.of(3);
// Container(3)

Container.of('hotdogs');
// Container("hotdogs")

Container.of(Container.of({ name: 'yoda' }));
// Container(Container({ name: 'yoda' }))

In Node.js sehen Sie »{ $value: x }«, obwohl wir »Container(x)« erstellt haben. Chrome zeigt den Typ korrekt an. Solange wir das Container-Konzept verstehen, sind wir auf dem richtigen Weg. In manchen Umgebungen können Sie die »inspect«-Methode überschreiben. Für dieses Buch schreiben wir die konzeptionelle Ausgabe, als hätten wir »inspect« überschrieben – dies ist didaktisch und ästhetisch sinnvoller als »{ $value: x }«.

Klären wir einige Punkte, bevor wir fortfahren:

  • »Container« ist ein Objekt mit einer Eigenschaft. Viele Container halten nur ein Element, sind aber nicht darauf beschränkt. Wir haben die Eigenschaft willkürlich »$value« genannt.

  • Der »$value« darf kein spezifischer Typ sein, sonst würde der Container seinem Namen nicht gerecht.

  • Einmal im Container, bleiben die Daten dort. Theoretisch könnten wir sie mit ».$value« entnehmen, aber das würde den Zweck verfehlen.

Die Gründe hierfür werden bald glasklar werden. Vorläufig bitte ich um etwas Geduld.

Mein erster Funktor

Sobald unser Wert – egal welchen Typs – im Container ist, benötigen wir eine Methode, um Funktionen darauf anzuwenden.

js
// (a -> b) -> Container a -> Container b
Container.prototype.map = function (f) {
  return Container.of(f(this.$value));
};

Verhalten ähnlich Arrays »map«: Für »Container a« statt »[a]«. Die Funktionsweise ist im Wesentlichen identisch:

js
Container.of(2).map(two => two + 2); 
// Container(4)

Container.of('flamethrowers').map(s => s.toUpperCase()); 
// Container('FLAMETHROWERS')

Container.of('bombs').map(append(' away')).map(prop('length')); 
// Container(10)

Wir können mit unserem Wert arbeiten, ohne den Container zu verlassen. Dies ist bemerkenswert: Der Wert wird an die »map«-Funktion übergeben, verarbeitet und sicher zurück in den Container gelegt. Durch diesen Mechanismus können wir beliebig viele »map«-Operationen verketten, selbst bei Typänderungen (wie im dritten Beispiel gezeigt).

Moment – kontinuierliches »map«-Aufrufen ähnelt einer Komposition! Welche mathematische Magie wirkt hier? Wir haben soeben Funktoren entdeckt.

Ein Funktor ist ein Typ mit »map«-Implementierung, der bestimmte Gesetze befolgt.

Ja, ein »Functor« ist einfach ein Interface mit Vertrag. Funktoren stammen aus der Kategorientheorie – die mathematischen Details folgen später. Zunächst arbeiten wir mit Intuition und praktischen Anwendungen.

Warum sollten wir Werte verpacken und per »map« zugreifen? Die Antwort: Abstraktion der Funktionsanwendung. Durch »map« delegieren wir die Ausführung an den Container – ein äußerst mächtiges Konzept.

Schrödingers Maybe

cool cat, need reference

Der Basis-»Container« ist langweilig (oft »Identity« genannt, analog zur »id«-Funktion). Andere Funktoren wie »Maybe« bieten nützliches Zusatzverhalten während des Mappens. Lassen Sie uns diesen definieren.

Eine vollständige Implementierung findet sich in Anhang B.

js
class Maybe {
  static of(x) {
    return new Maybe(x);
  }

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  constructor(x) {
    this.$value = x;
  }

  map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  inspect() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }
}

»Maybe« prüft vor Funktionsaufruf, ob ein Wert vorhanden ist – elegant umgehen wir »null«-Probleme (vereinfachte Implementierung zu Lehrzwecken).

js
Maybe.of('Malkovich Malkovich').map(match(/a/ig));
// Just(True)

Maybe.of(null).map(match(/a/ig));
// Nothing

Maybe.of({ name: 'Boris' }).map(prop('age')).map(add(10));
// Nothing

Maybe.of({ name: 'Dinah', age: 14 }).map(prop('age')).map(add(10));
// Just(24)

Dank »Maybe« stürzt die Anwendung nicht ab, selbst bei »null«-Werten. Jede Funktionsanwendung enthält eine Werteprüfung.

Die Punktnotation ist akzeptabel, aber aus Gründen aus Teil 1 bevorzugen wir pointfree-Stil. »map« kann mit jedem Funktor arbeiten:

js
// map :: Functor f => (a -> b) -> f a -> f b
const map = curry((f, anyFunctor) => anyFunctor.map(f));

Dies ermöglicht nahtlose Komposition. Ramdas »map« verhält sich ähnlich. Beachten Sie den Typzusatz »Functor f =>«: »f« muss hier ein Funktor sein.

Anwendungsfälle

In der Praxis wird »Maybe« oft bei potentiell fehlschlagenden Funktionen eingesetzt.

js
// safeHead :: [a] -> Maybe(a)
const safeHead = xs => Maybe.of(xs[0]);

// streetName :: Object -> Maybe String
const streetName = compose(map(prop('street')), safeHead, prop('addresses'));

streetName({ addresses: [] });
// Nothing

streetName({ addresses: [{ street: 'Shady Ln.', number: 4201 }] });
// Just('Shady Ln.')

»safeHead« ist eine typsichere Variante von »head«. Durch »Maybe« werden wir gezwungen, »null«-Werte explizit zu behandeln. Die Funktion signalisiert potenzielles Scheitern klar durch ihren Rückgabetyp. Der Wert bleibt im »Maybe« geschützt – wir müssen »map« verwenden, um darauf zuzugreifen. Dies ist eine durch »safeHead« erzwungene »null«-Prüfung, die unsere Anwendung robust macht.

Manche Funktionen geben bewusst »Nothing« bei Fehlern zurück:

js
// withdraw :: Number -> Account -> Maybe(Account)
const withdraw = curry((amount, { balance }) =>
  Maybe.of(balance >= amount ? { balance: balance - amount } : null));

// This function is hypothetical, not implemented here... nor anywhere else.
// updateLedger :: Account -> Account 
const updateLedger = account => account;

// remainingBalance :: Account -> String
const remainingBalance = ({ balance }) => `Your balance is $${balance}`;

// finishTransaction :: Account -> String
const finishTransaction = compose(remainingBalance, updateLedger);


// getTwenty :: Account -> Maybe(String)
const getTwenty = compose(map(finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// Just('Your balance is $180')

getTwenty({ balance: 10.00 });
// Nothing

»withdraw« gibt »Nothing« bei unzureichendem Guthaben zurück. Durch »map« wird die weitere Verarbeitung abgebrochen – »finishTransaction« wird bei Fehlern nicht aufgerufen. So vermeiden wir inkonsistente Zustände wie falsche Kontostände.

Wertefreigabe

Wichtig: Irgendwann muss der Wert die Container verlassen (z.B. für Ausgaben). Ohne beobachtbare Effekte wäre das Programm wertlos – wie ein Zen-Koan: „Wenn ein Programm keine Wirkung zeigt – läuft es dann überhaupt?“.

Die Anwendung verarbeitet Daten bis zur finalen Ausgabe. Der Wert bleibt bis zum Schluss im Container – ein Herausziehen würde Schrödingers Katzenparadoxon ähnlich: Der Zustand bleibt unbestimmt bis zur finalen Operation. Dies ermöglicht linearen Datenfluss trotz verzweigter Logik.

Notausstieg: Mit »maybe« können wir Standardwerte definieren:

js
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = curry((v, f, m) => {
  if (m.isNothing) {
    return v;
  }

  return f(m.$value);
});

// getTwenty :: Account -> String
const getTwenty = compose(maybe('You\'re broke!', finishTransaction), withdraw(20));

getTwenty({ balance: 200.00 }); 
// 'Your balance is $180.00'

getTwenty({ balance: 10.00 }); 
// 'You\'re broke!'

»maybe« wirkt wie eine »if/else«-Verzweigung. Im Gegensatz dazu entspricht »map« einem »if (x !== null) { return f(x) }«.

Anfängliche Umgewöhnung bei »Maybe« ist normal (Swift/Scala-Entwickler kennen »Option«). Mit der Zeit wird die Sicherheit geschätzt – sie verhindert unsauberen Code.

Unsauberer Code ist wie bemälte Eier im Straßenverkehr oder strohgedämmte Altersheime. »Maybe« ist unsere Sicherheitsvorkehrung.

Hinweis: Echte Implementierungen trennen »Maybe« in »Just(x)«/»Nothing«. Dies respektiert parametrische Polymorphie und erlaubt »null«/»undefined«-Werte im Funktor.

Pure Fehlerbehandlung

pick a hand... need a reference

»throw/catch« ist nicht rein. Bei Fehlern wird die Kontrolle entzogen. Mit »Either« können wir höflich reagieren:

Vollständige Implementierung in Anhang B.

js
class Either {
  static of(x) {
    return new Right(x);
  }

  constructor(x) {
    this.$value = x;
  }
}

class Left extends Either {
  map(f) {
    return this;
  }

  inspect() {
    return `Left(${inspect(this.$value)})`;
  }
}

class Right extends Either {
  map(f) {
    return Either.of(f(this.$value));
  }

  inspect() {
    return `Right(${inspect(this.$value)})`;
  }
}

const left = x => new Left(x);

»Left« und »Right« sind Subklassen von »Either«. Praktisch ist »Left« für Fehler, »Right« für Erfolge. Sehen wir uns die Interaktion an:

js
Either.of('rain').map(str => `b${str}`); 
// Right('brain')

left('rain').map(str => `It's gonna ${str}, better bring your umbrella!`); 
// Left('rain')

Either.of({ host: 'localhost', port: 80 }).map(prop('host'));
// Right('localhost')

left('rolls eyes...').map(prop('host'));
// Left('rolls eyes...')

»Left« ignoriert »map«-Aufrufe, »Right« verhält sich wie »Container«. Die Stärke liegt in der Fehlereinbettung bei »Left«.

Beispiel: Altersberechnung mit Fehlermeldung. Statt »Nothing« liefert »Either« erklärende Fehler:

js
const moment = require('moment');

// getAge :: Date -> User -> Either(String, Number)
const getAge = curry((now, user) => {
  const birthDate = moment(user.birthDate, 'YYYY-MM-DD');

  return birthDate.isValid()
    ? Either.of(now.diff(birthDate, 'years'))
    : left('Birth date could not be parsed');
});

getAge(moment(), { birthDate: '2005-12-12' });
// Right(9)

getAge(moment(), { birthDate: 'July 4, 2001' });
// Left('Birth date could not be parsed')

Bei ungültigem Datum erhalten wir eine Fehlermeldung in »Left« – sauberer als Exceptions. Der Typ »Either(String, Number)« zeigt mögliche Rückgabewerte an (informell, da Basisklasse fehlt).

js
// fortune :: Number -> String
const fortune = compose(concat('If you survive, you will be '), toString, add(1));

// zoltar :: User -> Either(String, _)
const zoltar = compose(map(console.log), map(fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// Right(undefined)

zoltar({ birthDate: 'balloons!' });
// Left('Birth date could not be parsed')

Bei validem Datum erfolgt die Ausgabe. »Either« verhält sich wie eine kontrollierte Exception ohne Geschrei. Die »console.log«-Ausgabe sollte eigentlich außerhalb erfolgen – dient hier der Veranschaulichung.

Der »_«-Platzhalter zeigt einen zu ignorierenden Wert an (in manchen Browsern benötigt »console.log.bind(console)«).

Wichtig: Funktionen wie »fortune« bleiben funktoragnostisch. Erst durch »map« werden sie »angehoben« (Lifting). Dies fördert Wiederverwendbarkeit.

»Either« eignet sich für Validierungen und kritische Fehler. Ersetzen Sie »Maybe« durch »Either« für besseres Feedback.

»Either« repräsentiert logische Disjunktion (»||«) und ist kategorientheoretisch ein Coprodukt. Als Summentyp kombiniert es Mengen. Hauptanwendung bleibt Fehlerbehandlung.

Ähnlich »maybe«: »either« nimmt zwei Funktionen (Fehler/Erfolg):

js
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = curry((f, g, e) => {
  let result;

  switch (e.constructor) {
    case Left:
      result = f(e.$value);
      break;

    case Right:
      result = g(e.$value);
      break;

    // No Default
  }

  return result;
});

// zoltar :: User -> _
const zoltar = compose(console.log, either(id, fortune), getAge(moment()));

zoltar({ birthDate: '2005-12-12' });
// 'If you survive, you will be 10'
// undefined

zoltar({ birthDate: 'balloons!' });
// 'Birth date could not be parsed'
// undefined

»id« gibt den »Left«-Wert zurück. So bauen wir robuste Anwendungen mit sauberem Fehlerhandling. Nun zu einem neuen Funktor-Typ.

Old McDonald Had Effects...

dominoes.. need a reference

Aus Kapitel über Reinheit: Seiteneffekte können durch Funktionsummantelung rein werden:

js
// getFromStorage :: String -> (_ -> String)
const getFromStorage = key => () => localStorage[key];

Durch die Wrapper-Funktion wird »getFromStorage« deterministisch: Immer gleiche Ausgabe bei gleicher Eingabe – eine Funktion, die bei Aufruf den Wert aus »localStorage« holt.

Praktisch unbrauchbar – wie Spielzeug in Originalverpackung. Die Lösung: »IO«.

js
class IO {
  static of(x) {
    return new IO(() => x);
  }

  constructor(fn) {
    this.$value = fn;
  }

  map(fn) {
    return new IO(compose(fn, this.$value));
  }

  inspect() {
    return `IO(${inspect(this.$value)})`;
  }
}

»IO« speichert Effekte als Funktion (technisches Detail). Der »$value« repräsentiert das zukünftige Ergebnis. »IO.of« vermeidet vorzeitige Auswertung. Konzeptionell schreiben wir den hypothetischen Wert – tatsächlicher Wert bleibt bis zur Ausführung unbekannt.

Praktisches Beispiel:

js
// ioWindow :: IO Window
const ioWindow = new IO(() => window);

ioWindow.map(win => win.innerWidth);
// IO(1430)

ioWindow
  .map(prop('location'))
  .map(prop('href'))
  .map(split('/'));
// IO(['http:', '', 'localhost:8000', 'blog', 'posts'])


// $ :: String -> IO [DOM]
const $ = selector => new IO(() => document.querySelectorAll(selector));

$('#myDiv').map(head).map(div => div.innerHTML);
// IO('I am some inner html')

»ioWindow« ist ein direkt mappbares »IO«. Jedes »map« hängt Funktionen an eine Kompositionskette – Dominosteine, die erst beim Auslösen fallen. Dies ähnelt dem Command-Pattern.

Funktor-Gesetze (später behandelt) ermöglichen intuitives Mappen. So behalten wir Reinheit trotz Effekten.

Doch wann lösen wir die Effekte aus? Die Verantwortung liegt beim Aufrufer:

js
// url :: IO String
const url = new IO(() => window.location.href);

// toPairs :: String -> [[String]]
const toPairs = compose(map(split('=')), split('&'));

// params :: String -> [[String]]
const params = compose(toPairs, last, split('?'));

// findParam :: String -> IO Maybe [String]
const findParam = key => map(compose(Maybe.of, find(compose(eq(key), head)), params), url);

// -- Impure calling code ----------------------------------------------

// run it by calling $value()!
findParam('searchTerm').$value();
// Just(['searchTerm', 'wafflehouse'])

Die Bibliothek übergibt die Effektausführung. Containerverschachtelungen wie »IO(Maybe([x]))« sind möglich und ausdrucksstark.

Warnung: »$value« ist eigentlich eine Granate – umbenannt zu »unsafePerformIO«:

js
class IO {
  constructor(io) {
    this.unsafePerformIO = io;
  }

  map(fn) {
    return new IO(compose(fn, this.unsafePerformIO));
  }
}

Aufruf wird zu »findParam('searchTerm').unsafePerformIO()« – klar und deutlich.

»IO« zähmt Effekte. Als Nächstes: Asynchrone Tasks.

Asynchrone Aufgaben

Die verschachtelte Callback-Hölle? Nein danke! Die Lösung beginnt mit »F«:

Verwendung von »Data.Task« aus Folktale:

js
// -- Node readFile example ------------------------------------------

const fs = require('fs');

// readFile :: String -> Task Error String
const readFile = filename => new Task((reject, result) => {
  fs.readFile(filename, (err, data) => (err ? reject(err) : result(data)));
});

readFile('metamorphosis').map(split('\n')).map(head);
// Task('One morning, as Gregor Samsa was waking up from anxious dreams, he discovered that
// in bed he had been changed into a monstrous verminous bug.')


// -- jQuery getJSON example -----------------------------------------

// getJSON :: String -> {} -> Task Error JSON
const getJSON = curry((url, params) => new Task((reject, result) => {
  $.getJSON(url, params, result).fail(reject);
}));

getJSON('/video', { id: 10 }).map(prop('title'));
// Task('Family Matters ep 15')


// -- Default Minimal Context ----------------------------------------

// We can put normal, non futuristic values inside as well
Task.of(3).map(three => three + 1);
// Task(4)

»map« arbeitet auf zukünftigen Werten – vertraut aus Promises (»then«).

Promises sind unrein – aber die Analogie hilft.

Wie »IO« wartet »Task« auf Startkommando. Asynchrone Lesevorgänge sind mit »Task« bereits rein. »map« arbeitet mit Zukunftsanweisungen – technische Prokrastination.

Starten mit »fork« (nicht-blockierend):

js
// -- Pure application -------------------------------------------------
// blogPage :: Posts -> HTML
const blogPage = Handlebars.compile(blogTemplate);

// renderPage :: Posts -> HTML
const renderPage = compose(blogPage, sortBy(prop('date')));

// blog :: Params -> Task Error HTML
const blog = compose(map(renderPage), getJSON('/posts'));


// -- Impure calling code ----------------------------------------------
blog({}).fork(
  error => $('#error').html(error.message),
  page => $('#main').html(page),
);

$('#spinner').show();

»fork« startet die Anfrage, zeigt einen Ladeindikator und verarbeitet später Erfolg/Fehler.

Linearer Kontrollfluss trotz Asynchronität – einfacher zu verfolgen als Callback-Wirrwarr.

»Task« integriert »Either« für Fehlerbehandlung in asynchronem Kontext.

Trotzdem bleiben »IO« und »Either« nützlich:

js
// Postgres.connect :: Url -> IO DbConnection
// runQuery :: DbConnection -> ResultSet
// readFile :: String -> Task Error String

// -- Pure application -------------------------------------------------

// dbUrl :: Config -> Either Error Url
const dbUrl = ({ uname, pass, host, db }) => {
  if (uname && pass && host && db) {
    return Either.of(`db:pg://${uname}:${pass}@${host}5432/${db}`);
  }

  return left(Error('Invalid config!'));
};

// connectDb :: Config -> Either Error (IO DbConnection)
const connectDb = compose(map(Postgres.connect), dbUrl);

// getConfig :: Filename -> Task Error (Either Error (IO DbConnection))
const getConfig = compose(map(compose(connectDb, JSON.parse)), readFile);


// -- Impure calling code ----------------------------------------------

getConfig('db.json').fork(
  logErr('couldn\'t read file'),
  either(console.log, map(runQuery)),
);

»readFile« nutzt »Task« für Asynchronität, während »Either« und »IO« synchrone Aufgaben übernehmen.

So einfach ist »map«.

Für komplexe Workflows benötigen wir später Monaden. Zuerst die mathematische Basis.

Ein Hauch von Theorie

Funktoren folgen Identitäts- und Kompositionsgesetzen:

js
// identity
map(id) === id;

// composition
compose(map(f), map(g)) === map(compose(f, g));

Identitätsgesetz (überprüfbar im Code):

js
const idLaw1 = map(id);
const idLaw2 = id;

idLaw1(Container.of(2)); // Container(2)
idLaw2(Container.of(2)); // Container(2)

Kompositionsgesetz:

js
const compLaw1 = compose(map(append(' world')), map(append(' cruel')));
const compLaw2 = map(compose(append(' world'), append(' cruel')));

compLaw1(Container.of('Goodbye')); // Container('Goodbye cruel world')
compLaw2(Container.of('Goodbye')); // Container('Goodbye cruel world')

Kategorientheorie: Funktoren erhalten Identität und Komposition. Unsere Implementierungen müssen diese Gesetze einhalten.

Kategorien sind Netzwerke aus Objekten und Morphismen. Ein Funktor bildet eine Kategorie auf eine andere ab (mit Objekt F a für a ∈ C).

Categories mapped

»Maybe« bildet Typen auf mögliche Nichtexistenz ab. Jeder Morphismus enthält Nullprüfung. Dies entspricht einer Subkategorie.

Visualisierung: Diagramme zeigen kommutierende Pfade (unterschiedliche Routen, gleiches Ergebnis).

functor diagram

Formale Eigenschaften ermöglichen Code-Transformationen ohne Einzelfallprüfung.

js
// topRoute :: String -> Maybe String
const topRoute = compose(Maybe.of, reverse);

// bottomRoute :: String -> Maybe String
const bottomRoute = compose(map(reverse), Maybe.of);

topRoute('hi'); // Just('ih')
bottomRoute('hi'); // Just('ih')

Diagramm:

functor diagram 2

Refaktorierungen basierend auf Funktoreigenschaften.

Verschachtelte Funktoren:

js
const nested = Task.of([Either.of('pillows'), left('no sleep for you')]);

map(map(map(toUpperCase)), nested);
// Task([Right('PILLOWS'), Left('no sleep for you')])

Dreifach verschachtelt: Future von Array von Either. »map(map(map(f)))« überwinden durch Funktorkomposition:

js
class Compose {
  constructor(fgx) {
    this.getCompose = fgx;
  }

  static of(fgx) {
    return new Compose(fgx);
  }

  map(fn) {
    return new Compose(map(map(fn), this.getCompose));
  }
}

const tmd = Task.of(Maybe.of('Rock over London'));

const ctmd = Compose.of(tmd);

const ctmd2 = map(append(', rock on, Chicago'), ctmd);
// Compose(Task(Just('Rock over London, rock on, Chicago')))

ctmd2.getCompose;
// Task(Just('Rock over London, rock on, Chicago'))

Identitätsfunktor und assoziative Komposition bilden eine Kategorie. Architektonisch tiefgründig.

Zusammenfassung

Funktoren sind allgegenwärtig: Bäume, Listen, Ereignisströme etc. Werden in diesem Buch extensiv genutzt.

Offene Fragen: Multifunktor-Argumente, asynchrone Sequenzen? Kommen mit Monaden.

Kapitel 09: Monadische Zwiebeln

Übungen

Let's Practice!

Nutzen Sie »add« und »map«, um einen Wert im Funktor zu inkrementieren. // incrF :: Functor f => f Int -> f Int const incrF = undefined;


Gegeben ist folgendes User-Objekt:

js
const user = { id: 2, name: 'Albert', active: true };

Let's Practice!

Finden Sie mit »safeProp« und »head« den ersten Initial des Users. // initial :: User -> Maybe String const initial = undefined;


Gegeben sind folgende Hilfsfunktionen:

js
// showWelcome :: User -> String
const showWelcome = compose(concat('Welcome '), prop('name'));

// checkActive :: User -> Either String User
const checkActive = function checkActive(user) {
  return user.active
    ? Either.of(user)
    : left('Your account is not active');
};

Let's Practice!

Schreiben Sie eine Funktion, die »checkActive« und »showWelcome« nutzt, um Zugriff zu gewähren oder Fehler zurückzugeben. // eitherWelcome :: User -> Either String String const eitherWelcome = undefined;


Nun betrachten wir folgende Funktionen:

js
// validateUser :: (User -> Either String ()) -> User -> Either String User
const validateUser = curry((validate, user) => validate(user).map(_ => user));

// save :: User -> IO User
const save = user => new IO(() => ({ ...user, saved: true }));

Let's Practice!

Schreiben Sie »validateName« zur Namenslängenprüfung (≥3 Zeichen). Nutzen Sie »either«, »showWelcome« und »save« für eine Registrierungsfunktion. Hinweis: Beide »either«-Argumente müssen denselben Typ zurückgeben. // validateName :: User -> Either String () const validateName = undefined; // register :: User -> IO String const register = compose(undefined, validateUser(validateName));