Toimijat ja luokat

Säveltämisohjelmisto

Tupakointikuutiot savuksi - MattysFlicks - (CC BY 2.0)
Huomaa: Tämä on osa ”Composing Software” -sarjaa (nyt kirja!), Joka käsittelee funktionaalisen ohjelmoinnin ja koostumustekniikan tekniikoiden käyttöä JavaScript ES6 +: ssa alusta alkaen. Pysy kanavalla. Tulevaa on paljon enemmän!
>

Functor-tietotyyppi on jotain, jonka voit kartoittaa. Se on säilö, jossa on käyttöliittymä, jota voidaan käyttää toiminnon käyttämiseen sen sisällä oleviin arvoihin. Kun näet functorin, sinun pitäisi ajatella ”kartoitettavissa”. Toimintotyypit esitetään tyypillisesti objektina .map () -menetelmällä, joka kartoittaa tuloista lähtöihin säilyttäen samalla rakenteen. Käytännössä ”rakenteen säilyttäminen” tarkoittaa, että palautusarvo on saman tyyppinen toimija (tosin arvot säiliön sisällä voivat olla eri tyyppisiä).

Functor toimittaa laatikon, jossa on nolla tai enemmän asioita, ja kartoitusliittymän. Matriisi on hyvä esimerkki funktorista, mutta myös monia muita esineitä voidaan kartoittaa, mukaan lukien lupaukset, purot, puut, esineet jne. JavaScriptin sisäänrakennettu taulukko ja lupausobjektit toimivat kuten funktorit. Kokoelmissa (taulukot, virrat jne.) .Map () iteroi tyypillisesti kokoelman yli ja soveltaa annettua toimintoa jokaiseen kokoelman arvoon, mutta ei kaikki funktorit toistuvat. Funktorit tarkoittavat todella funktion käyttämistä tietyssä yhteydessä.

Lupaukset käyttävät nimeä .then () .map (): n sijasta. Voit yleensä ajatella .then () asynkronisena .map () -menetelmänä, paitsi jos sinulla on sisäkkäinen lupaus, jolloin se purkaa ulkoisen lupauksen automaattisesti. Jälleen arvoille, jotka eivät ole lupauksia,. Then () toimii kuin asynkroninen .map (). Arvoille, jotka ovat itse lupauksia,. Then () toimii kuten monadien (.flatMap ()) menetelmä (joskus kutsutaan myös .chain ()). Joten lupaukset eivät ole aivan toimijoita, eivätkä aivan monadeja, mutta käytännössä voit yleensä kohdella heitä joko. Älä vielä huolehdi siitä, mitkä monadit ovat. Monadit ovat eräänlainen functor, joten sinun on ensin opittava functors.

On olemassa paljon kirjastoja, jotka tekevät monista muistakin asioista toimijoita.

Haskellissa funktiotyyppi on määritelty seuraavasti:

fmap :: (a -> b) -> f a -> f b

Annetaan funktio, joka vie a: n ja palauttaa b: n ja funktorin, jonka sisällä on nolla tai enemmän: fmap palauttaa ruudun, jonka sisällä on nolla tai enemmän bs. Faa- ja fb-bittejä voidaan lukea sanoilla “a-toiminto” ja “b-toimija”, mikä tarkoittaa, että f a: lla on kuin laatikossa ja f b: llä on bs laatikon sisällä.

Functorin käyttö on helppoa - soita vain kartta ():

const f = [1, 2, 3];
f.map (double); // [2, 4, 6]

Functor-lait

Ryhmillä on kaksi tärkeää ominaisuutta:

  1. identiteetti
  2. Sävellys

Koska functor on kartoitus luokkien välillä, functorien on kunnioitettava identiteettiä ja sävellystä. Yhdessä heidät tunnetaan toimijalakeina.

identiteetti

Jos siirrät identiteettitoiminnon (x => x) kohtaan f.map (), jossa f on jokin funktio, tuloksen tulisi olla vastaava (samalla merkityksellä kuin) f:

const f = [1, 2, 3];
f.kartta (x => x); // [1, 2, 3]

Sävellys

Toimijoiden on noudatettava koostumuslakia: F.map (x => f (g (x))) vastaa F.map (g) .map (f).

Funktion koostumus on yhden funktion soveltaminen toisen tulokseen, esim. Annettuna x ja funktiot, f ja g, koostumus (f ∘ g) (x) (yleensä lyhennetty arvoon f ∘ g - (x) on tarkoittaa (f (g (x))).

Paljon toiminnallisia ohjelmointitermejä tulee kategoriateoriasta, ja kategoriateorian ydin on koostumus. Luokkateoria on aluksi pelottava, mutta helppo. Kuten hyppääminen sukeltavalta pois tai vuoristorata. Tässä on luokkateorian perusta muutamassa luettelossa:

  • Luokka on kokoelma esineitä ja nuolet esineiden välillä (missä ”esine” voi tarkoittaa kirjaimellisesti mitä tahansa).
  • Nuolet tunnetaan morfismeina. Morfismit voidaan ajatella ja esittää koodissa funktiona.
  • Kaikilla kytkettyjen objektien ryhmällä, a -> b -> c, on oltava koostumus, joka menee suoraan kohdasta a -> c.
  • Kaikkia nuolia voidaan esittää koostumuksina (vaikka se olisi vain koostumus, jolla on objektin identiteetin nuoli). Kaikilla luokan kohteilla on identiteetin nuolet.

Sano, että sinulla on funktio g, joka vie a: n ja palauttaa b: n, ja toinen funktio f, joka vie b: n ja palauttaa c: n; siellä on oltava myös funktio h, joka edustaa f: n ja g: n koostumusta. Joten koostumus kohdasta a -> c on koostumus f ∘ g (f g jälkeen). Joten, h (x) = f (g (x)). Funktion koostumus toimii oikealta vasemmalle, ei vasemmalta oikealle, minkä vuoksi f ∘ g: tä kutsutaan usein f: ksi g: n jälkeen.

Koostumus on assosiatiivinen. Pohjimmiltaan se tarkoittaa, että kun sävelet useita toimintoja (morfismit, jos tunnet fyysistä), et tarvitse sulkuja:

h∘ (g∘f) = (h∘g) ∘f = h∘g∘f

Katsotaanpa taas JavaScriptin sävellyslakia:

Toiminto, F:

Const F = [1, 2, 3];

Seuraavat ovat vastaavia:

F.map (x => f (g (x)));
// vastaa ...
F.map (g) .map (f);

Endofunctors

Endofunctor on functor, joka kartoittaa luokasta takaisin samaan luokkaan.

Toiminto voi kartoittaa luokista luokkiin: X -> Y

Päätefunkeri karttaa luokasta samaan luokkaan: X -> X

Monadi on endofunktori. Muistaa:

”Monadi on vain monoidi endofunktoreiden luokassa. Mikä on ongelma?"

Toivottavasti sillä tarjouksella on alkanut olla enemmän järkeä. Pääset myöhemmin monoideihin ja monadeihin.

Luo oma toimittajasi

Tässä on yksinkertainen esimerkki functorista:

const Identiteetti = arvo => ({
  kartta: fn => Identiteetti (fn (arvo))
});

Kuten näette, se täyttää funktiolait:

// trace () on apuohjelma, jonka avulla voit helposti tarkastaa
// sisältö.
const trace = x => {
  console.log (x);
  paluu x;
};
const u = identiteetti (2);
// Henkilöllisyyslaki
u.map (jälki); // 2
u.map (x => x) .map (jäljitys); // 2
const f = n => n + 1;
const g = n => n * 2;
// Kokoonpanolaki
const r1 = u.map (x => f (g (x)));
const r2 = u.map (g) .map (f);
r1.map (jälki); // 5
r2.map (jälki); // 5

Nyt voit karttaa minkä tahansa tietotyypin yli, aivan kuten voit karttaa taulukon yli. Kiva!

Se on suunnilleen yhtä helppoa kuin functor voi saada JavaScriptin, mutta puuttuu joitain ominaisuuksia, joita odotamme JavaScriptin tietotyypeiltä. Lisäämme ne. Eikö olisi hienoa, jos + operaattori pystyisi työskentelemään numero- ja merkkijonoarvojen kanssa?

Tämän työn tekeminen edellyttää, että toteutamme .valueOf () -, joka vaikuttaa myös kätevältä tavasta purkaa arvo toimintoon:

const Identiteetti = arvo => ({
  kartta: fn => Identiteetti (fn (arvo)),
  valueOf: () => arvo,
});
const ints = (identiteetti (2) + identiteetti (4));
jäljittää (ints); // 6
const hi = (Identiteetti ('h') + Identiteetti ('i'));
jäljittää (hi); // "Hei"

Kiva. Mutta entä jos haluamme tarkastaa identiteetti-ilmentymän konsolissa? Olisi hienoa, jos siinä sanotaan "Identiteetti (arvo)", eikö niin. Lisäämme .toString () -menetelmä:

toString: () => `Identiteetti ($ {arvo})`,

Viileä. Meidän pitäisi todennäköisesti ottaa käyttöön myös standardi JS-iterointiprotokolla. Voimme tehdä sen lisäämällä mukautetun iteraattorin:

[Symbol.iterator]: toiminto * () {
  saantoarvo;
}

Nyt tämä toimii:

// [Symbol.iterator] mahdollistaa standardit JS-iteraatiot:
const arr = [6, 7, ... Identiteetti (8)];
jäljittää (sov); // [6, 7, 8]

Entä jos haluat ottaa identiteetin (n) ja palauttaa joukon henkilöllisyyksiä, jotka sisältävät n + 1, n + 2 ja niin edelleen? Helppoa, eikö?

const fRange = (
  alkaa,
  pää
) => Array.from (
  {pituus: loppu - alku + 1},
  (x, i) => Identiteetti (i + alku)
);

Ah, mutta entä jos haluat tämän toimivan minkä tahansa toimijan kanssa? Entä jos meillä olisi spesifikaatio, joka sanoi, että jokaisella tietotyypin esiintymällä on oltava viittaus sen rakentajaan? Sitten voit tehdä tämän:

const fRange = (
  alkaa,
  pää
) => Array.from (
  {pituus: loppu - alku + 1},
  
  // Muuta `Identiteetti` arvoksi` aloita.rakentaja`
  (x, i) => start.constructor (i + start)
);
vakioalue = fRange (identiteetti (2), 4);
alue.kartta (x => x.kartta (jäljitys)); // 2, 3, 4

Entä jos haluat testata onko arvo funktio? Voisimme lisätä staattisen menetelmän henkilöllisyyteen tarkistaaksesi. Meidän pitäisi heittää staattinen .toString (), kun olemme siinä:

Object.assign (Identiteetti, {
  toString: () => 'Identiteetti',
  on: x => tyypin x.map === 'toiminto'
});

Laitetaan kaikki tämä yhteen:

const Identiteetti = arvo => ({
  kartta: fn => Identiteetti (fn (arvo)),
  valueOf: () => arvo,
  toString: () => `Identiteetti ($ {arvo})`,
  [Symbol.iterator]: toiminto * () {
    saantoarvo;
  },
  rakentaja: Identiteetti
});
Object.assign (Identiteetti, {
  toString: () => 'Identiteetti',
  on: x => tyypin x.map === 'toiminto'
});

Huomaa, että sinun ei tarvitse kaikkia näitä ylimääräisiä tavaroita, jotta jotain voidaan luokitella toimijaksi tai endofunktoriksi. Se on ehdottomasti mukavuuden vuoksi. Kaikki mitä tarvitset functoriin, on .map () -liittymä, joka täyttää functor-lait.

Miksi toimittajat?

Funktorit ovat upeita monista syistä. Tärkeintä on, että ne ovat abstraktio, jonka avulla voit toteuttaa monia hyödyllisiä asioita tavalla, joka toimii minkä tahansa tietotyypin kanssa. Entä esimerkiksi jos haluat aloittaa toimintaketjun, mutta vain jos funktiorivin arvo ei ole määrittelemätön tai nolla?

// Luo predikaatti
vakio on olemassa = x => (x.valueOf ()! == määrittelemätön && x.valueOf ()! == nolla);
const ifExists = x => ({
  kartta: fn => onko olemassa (x)? x.map (fn): x
});
const add1 = n => n + 1;
const double = n => n * 2;
// Mitään ei tapahdu...
ifExists (Identity (määrittelemätön)). kartta (jälki);
// Ei vieläkään mitään...
ifExists (Identity (nolla)). kartta (jälki);
// 42
ifExists (Identity (20))
  .map (ADD1)
  .map (double)
  .map (jälki)
;

Tietysti toiminnallisessa ohjelmoinnissa on kyse pienten toimintojen säveltämisestä korkeamman tason abstraktioiden luomiseksi. Entä jos haluat yleisen kartan, joka toimii minkä tahansa toimijan kanssa? Tällä tavoin voit käyttää osittain argumentteja luodaksesi uusia toimintoja.

Helppo. Valitse suosikki auto-currysi tai käytä tätä taikuuskäyttöä aikaisemmin:

const curry = (
  f, arr = []
) => (... args) => (
  a => a.length === f.length?
    f (... a):
    curry (f, a)
) ([... arr, ... args]);

Nyt voimme mukauttaa karttaa:

const map = curry ((fn, F) => F. map (fn));
const double = n => n * 2;
const mdouble = kartta (kaksinkertainen);
mdouble (Identity (4)). kartta (jälki); // 8

johtopäätös

Toimijat ovat asioita, jotka voimme kartoittaa. Tarkemmin sanottuna functor on kartoitus luokkasta toiseen. Toiminto voi jopa kartoittaa luokasta takaisin samaan luokkaan (ts. Endofunktoriin).

Luokka on kokoelma esineitä, joissa nuolet ovat objektien välillä. Nuolet edustavat morfismeja (alias funktiot, aka koostumukset). Jokaisella luokan objektilla on identiteettimorfismi (x => x). Kaikille esineketjuille A -> B -> C on oltava koostumus A -> C.

Funktorit ovat hienoja korkeamman asteen abstraktioita, joiden avulla voit luoda erilaisia ​​yleisiä toimintoja, jotka toimivat kaikille tietotyypeille.

Seuraava: Toiminnalliset sekoitukset>

Tasoita taitojasi live-ohjauksella 1: 1

DevAnywhere on nopein tapa tasoittaa edistyneitä JavaScript-taitoja:

  • Live-oppitunnit
  • Joustavat tunnit
  • 1: 1 mentorointi
  • Luo todellisia tuotantosovelluksia
https://devanywhere.io/

Eric Elliott on kirjoittanut ”Programming JavaScript Applications” -sovelluksen (O’Reilly) ja on DevAnywhere.io: n perustaja. Hän on osallistunut ohjelmistokokemuksiin Adobe Systemsille, Zumba Fitnessille, The Wall Street Journalille, ESPN: lle, BBC: lle ja parhaimmille äänittäjille, kuten Usher, Frank Ocean, Metallica ja monille muille.

Hän työskentelee missä vain haluaa, maailman kauneimman naisen kanssa.