1. Erste Schritte
2. Kombinieren Sie die Funktionen
3. Teilweise Verwendung (Currying)
4. Deklarative Programmierung
5. Grundlegende Notation
6. Unveränderlichkeit und Gegenstände
7. Unveränderlichkeit und Arrays
8. Objektive
9. Fazit
Dieser Beitrag ist der sechste Teil einer Reihe von Artikeln zur funktionalen Programmierung mit dem Titel Ramda Style Thinking.
Im fünften Teil haben wir über das Schreiben von Funktionen im Stil der sinnlosen Notation gesprochen, wobei das Hauptargument mit den Daten für unsere Funktion nicht explizit angegeben ist.
Zu diesem Zeitpunkt konnten wir nicht alle unsere Funktionen in einem bitlosen Stil umschreiben, da wir nicht über die dafür erforderlichen Tools verfügten. Es ist Zeit, sie zu studieren.
Objekteigenschaften lesen
Schauen wir uns noch einmal das Beispiel der Definition von Personen mit Stimmrecht an, das wir im fünften Teil untersucht haben :
const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => person.age >= 18 const isCitizen = either(wasBornInCountry, wasNaturalized) const isEligibleToVote = both(isOver18, isCitizen)
Wie Sie sehen, schließen wir isCitizen
und isEligibleToVote
, können dies jedoch mit den ersten drei Funktionen nicht tun.
Wie wir im vierten Teil gelernt haben, können wir unsere Funktionen durch die Verwendung von equals und gte deklarativer machen . Beginnen wir damit:
const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY) const wasNaturalized = person => Boolean(person.naturalizationDate) const isOver18 = person => gte(person.age, 18)
Um diese Funktionen sinnlos zu machen, benötigen wir eine Möglichkeit, die Funktion so zu konstruieren, dass wir die person
am Ende des Ausdrucks anwenden. Das Problem ist, dass wir auf die Eigenschaften der person
zugreifen müssen, jetzt wissen wir, wie dies nur möglich ist - und das ist unbedingt erforderlich.
Stütze
Zum Glück hilft uns Ramda erneut. Es bietet eine Requisitenfunktion für den Zugriff auf die Eigenschaften von Objekten.
Mit prop
können wir person.birthCountry
in prop('birthCountry', person)
umschreiben. Lass es uns tun:
const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
Wow, jetzt sieht es viel schlimmer aus. Aber lassen Sie uns unser Refactoring fortsetzen. Lassen Sie uns die Reihenfolge der Argumente ändern, die wir an equals
damit die prop
letzter Stelle steht. equals
funktioniert genauso in umgekehrter Reihenfolge, sodass wir nichts kaputt machen:
const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(prop('age', person), 18)
Als nächstes verwenden wir Currying, die natürliche Eigenschaft von equals
und gte
, um neue Funktionen zu erstellen, für die das Ergebnis des prop
Aufrufs gilt:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person)) const wasNaturalized = person => Boolean(prop('naturalizationDate', person)) const isOver18 = person => gte(__, 18)(prop('age', person))
Es sieht immer noch nach der schlechtesten Option aus, aber fahren wir fort. Lassen Sie uns das Currying für alle prop
:
const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person)) const wasNaturalized = person => Boolean(prop('naturalizationDate')(person)) const isOver18 = person => gte(__, 18)(prop('age')(person))
Wieder irgendwie nicht sehr. Aber jetzt sehen wir ein bekanntes Muster. Alle unsere Funktionen haben das gleiche Bild f(g(person))
, und wie wir aus dem zweiten Teil wissen, entspricht dies der Zusammensetzung compose(f, g)(person)
.
Wenden wir diesen Vorteil auf unseren Code an:
const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person) const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person) const isOver18 = person => compose(gte(__, 18), prop('age'))(person)
Jetzt haben wir etwas. Alle unsere Funktionen sehen aus wie person => f(person)
. Und wir wissen bereits aus dem fünften Teil, dass wir diese Funktionen sinnlos machen können.
const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry')) const wasNaturalized = compose(Boolean, prop('naturalizationDate')) const isOver18 = compose(gte(__, 18), prop('age'))
Als wir anfingen, war es nicht offensichtlich, dass unsere Methoden zwei Dinge taten. Sie wandten sich der Eigenschaft des Objekts zu und bereiteten einige Operationen mit seinem Wert vor. Dieses Refactoring in einen sinnlosen Stil machte dies sehr deutlich.
Schauen wir uns einige der anderen Tools an, die Ramda für die Arbeit mit Objekten bereitstellt.
wählen
Wenn prop
eine Eigenschaft eines Objekts liest und seinen Wert zurückgibt, liest pick viele Eigenschaften aus dem Objekt und gibt nur mit ihnen ein neues Objekt zurück.
Wenn wir zum Beispiel nur die Namen und Jahre von Personen benötigen, können wir pick(['name','age'], person)
.
hat
Wenn wir nur wissen möchten, dass unser Objekt eine Eigenschaft hat, ohne seinen Wert zu lesen, können wir die has- Funktion verwenden, um seine Eigenschaften zu überprüfen, und hasIn , um die Prototypkette zu überprüfen: has('name', person)
.
Pfad
Wenn prop
eine Objekteigenschaft prop
, geht der Pfad tiefer in verschachtelte Objekte. Zum Beispiel möchten wir die Postleitzahl aus einer tieferen Struktur ziehen: path(['address','zipCode'], person)
.
Beachten Sie, dass der path
verzeihender ist als die prop
. path
gibt undefined
wenn etwas im Pfad (einschließlich des ursprünglichen Arguments) null
oder undefined
, während prop
in solchen Situationen einen Fehler verursacht.
propOr / pathOr
propOr und pathOr ähneln prop
und path
Kombination mit defaultTo
. Sie bieten Ihnen die Möglichkeit, einen Standardwert für eine Eigenschaft oder einen Pfad anzugeben, der / die im untersuchten Objekt nicht gefunden werden kann.
Beispielsweise können wir einen Platzhalter propOr('<Unnamed>, 'name', person)
wenn wir den Namen der Person nicht kennen: propOr('<Unnamed>, 'name', person)
. Beachten Sie, dass propOr
im Gegensatz zu prop
keinen Fehler verursacht, wenn person
null
oder undefined
. Stattdessen wird der Standardwert zurückgegeben.
Schlüssel / Werte
keys gibt ein Array zurück, das alle Namen aller bekannten Eigenschaften des Objekts enthält. Werte geben die Werte dieser Eigenschaften zurück. Diese Funktionen können nützlich sein, wenn sie mit den Iterationsfunktionen für Sammlungen kombiniert werden, die wir im ersten Teil kennengelernt haben .
Hinzufügen, Aktualisieren und Löschen von Eigenschaften
Jetzt haben wir viele Werkzeuge zum Lesen von Objekten in einem deklarativen Stil, aber was ist mit Änderungen?
Da Unveränderlichkeit für uns wichtig ist, möchten wir Objekte nicht direkt ändern. Stattdessen möchten wir neue Objekte zurückgeben, die sich wie gewünscht geändert haben.
Ramda bietet uns erneut viele Vorteile.
assoc / assocPath
Wenn wir in einem imperativen Stil programmieren, können wir den Namen der Person über den Zuweisungsoperator festlegen oder ändern: person.name = 'New name'
.
In unserer funktionalen, unveränderlichen Welt können wir stattdessen assoc verwenden: const updatedPerson = assoc('name', 'newName', person)
.
assoc
gibt ein neues Objekt mit einem hinzugefügten oder aktualisierten Eigenschaftswert zurück, wobei das ursprüngliche Objekt unverändert assoc
.
Wir haben auch assocPath zur Verfügung, um die angehängte Eigenschaft zu aktualisieren: const updatedPerson = assocPath(['address', 'zipCode'], '97504', person)
.
dissoc / dissocPath / weglassen
Was ist mit dem Löschen von Eigenschaften? Wir möchten delete person.age
. In Ramda verwenden wir dissoc : `const updatedPerson = dissoc ('age', person)
dissocPath ist ungefähr gleich, arbeitet jedoch mit tieferen Objektstrukturen: dissocPath(['address', 'zipCode'], person)
.
Und wir haben auch Auslassen , wodurch mehrere Eigenschaften gleichzeitig entfernt werden können: const updatedPerson = omit(['age', 'birthCountry'], person)
.
Bitte beachten Sie, dass pick
und omit
etwas ähnlich sind und sich sehr gut ergänzen. Sie eignen sich sehr gut für Whitelists (speichern Sie nur einen bestimmten Satz von Eigenschaften mit pick
) und Blacklists (um bestimmte Eigenschaften durch Auslassen zu omit
).
Jetzt wissen wir genug, um mit Objekten in einem deklarativen und unveränderlichen Stil zu arbeiten. Schreiben wir eine celebrateBirthday
Funktion, die das Alter der Person an ihrem Geburtstag aktualisiert.
const nextAge = compose(inc, prop('age')) const celebrateBirthday = person => assoc('age', nextAge(person), person)
Dies ist ein sehr verbreitetes Muster. Anstatt die Eigenschaft mit einem neuen Wert zu aktualisieren, möchten wir den Wert wirklich ändern, indem wir die Funktion wie hier auf den alten Wert anwenden.
Ich kenne keinen guten Weg, dies mit weniger Duplikaten und in einem weniger strengen Stil zu schreiben, wenn ich die Werkzeuge habe, die wir zuvor kennengelernt haben.
Ramda rettet uns erneut mit der Entwicklungsfunktion . evolve
akzeptiert ein Objekt und ermöglicht es Ihnen, Transformationsfunktionen für die Eigenschaften anzugeben, die wir ändern möchten. Lassen Sie uns celebrateBirthday
Geburtstag zu celebrateBirthday
indem Sie "Entwickeln" verwenden:
const celebrateBirthday = evolve({ age: inc })
Dieser Code besagt, dass wir das angegebene Objekt (das aufgrund des brutalen Stils nicht angezeigt wird) konvertieren, indem wir ein neues Objekt mit denselben Eigenschaften und Werten erstellen. Die Eigenschaft age
wird jedoch durch Anwenden von inc
auf den ursprünglichen Wert der Eigenschaft age
erhalten.
evolve
können viele Eigenschaften gleichzeitig und sogar auf mehreren Verschachtelungsebenen transformiert werden. Die Transformation des Objekts kann dasselbe Bild haben wie das veränderbare Objekt, und die evolve
wird unter Verwendung der Transformationsfunktionen in der angegebenen Form rekursiv zwischen den Strukturen übertragen.
Beachten Sie, dass evolve
keine neuen Eigenschaften hinzufügt. Wenn Sie eine Transformation für eine Eigenschaft angeben, die in dem zu verarbeitenden Objekt nicht vorkommt, wird sie von evolve
einfach ignoriert.
Ich habe festgestellt, dass die evolve
in meinen Anwendungen schnell zu einem Arbeitstier wird.
Objekte zusammenführen
Manchmal müssen Sie zwei Objekte miteinander kombinieren. Ein typischer Fall ist, wenn Sie eine Funktion haben, die benannte Optionen akzeptiert, und diese mit den Standardoptionen kombinieren möchten. Ramda bietet zu diesem Zweck eine Zusammenführungsfunktion .
function f(a, b, options = {}) { const defaultOptions = { value: 42, local: true } const finalOptions = merge(defaultOptions, options) }
merge
gibt ein neues Objekt zurück, das alle Eigenschaften und Werte beider Objekte enthält. Wenn beide Objekte dieselbe Eigenschaft haben, wird der Wert des zweiten Arguments erhalten.
Das Vorhandensein dieser Regel mit einem gewinnenden zweiten Argument macht es sinnvoll, merge
als eigenständiges Werkzeug zu verwenden, in Förderersituationen jedoch weniger aussagekräftig. In diesem Fall müssen Sie häufig eine Reihe von Transformationen für ein Objekt vorbereiten. Eine dieser Transformationen ist die Vereinigung einiger neuer Eigenschaftswerte. In diesem Fall soll das erste Argument anstelle des zweiten gewinnen.
Der Versuch, nur merge(newValues)
in der Pipeline zu verwenden, gibt nicht das, was wir gerne hätten.
In dieser Situation erstelle ich normalerweise mein eigenes Dienstprogramm namens reverseMerge
. Es kann als const reverseMerge = flip(merge)
. Der flip
Call tauscht die ersten beiden Argumente der für ihn geltenden Funktion aus.
merge
führt eine Oberflächenzusammenführung durch. Wenn Objekte in Kombination eine Eigenschaft haben, deren Wert ein Unterobjekt ist, werden diese Unterobjekte nicht zusammengeführt. Ramda verfügt derzeit nicht über eine Deep-Merge-Fähigkeit (Der Originalartikel, den ich übersetze, enthält bereits veraltete Informationen zu diesem Thema. Heute verfügt Ramda über Funktionen wie mergeDeepLeft , mergeDeepRight zum rekursiv tiefen Zusammenführen von Objekten und andere Methoden zum Zusammenführen. )
Beachten Sie, dass beim merge
nur zwei Argumente akzeptiert werden. Wenn Sie viele Objekte zu einem kombinieren möchten, können Sie mergeAll verwenden , für dessen Kombination ein Array von Objekten erforderlich ist.
Fazit
Heute haben wir eine wunderbare Reihe von Werkzeugen für die Arbeit mit Objekten in einem deklarativen und unveränderlichen Stil. Jetzt können wir Eigenschaften in Objekten lesen, hinzufügen, aktualisieren, löschen und transformieren, ohne die ursprünglichen Objekte zu ändern. Und wir können all diese Dinge in einem Stil tun, der es einfach macht, Funktionen miteinander zu kombinieren.
Weiter
Jetzt können wir mit Objekten in einem unveränderlichen Stil arbeiten, aber was ist mit Arrays? "Immunität und Arrays" werden uns sagen, was wir mit ihnen machen sollen.