Particularitatea limbajelor de programare funcționale este... Limbaje de programare funcționale. Implementare la cald

Limbajul de programare funcțional

Principalele proprietăți ale limbajelor de programare funcționale sunt de obicei considerate [ de cine?] următoarele:

  • concizie și simplitate;

Programele în limbaje funcționale sunt de obicei mult mai scurte și mai simple decât aceleași programe în limbaje imperative.
Exemplu (sortare rapidă Hoare într-un limbaj funcțional abstract):

QuickSort() =
quickSort() = quickSort(n | n t, n<= h) + [h] + quickSort (n | n t, n >h)

  • tastare puternică;

În limbajele funcționale, majoritatea erorilor pot fi corectate în etapa de compilare, astfel încât etapa de depanare și timpul general de dezvoltare a programului sunt reduse. În plus, tastarea puternică permite compilatorului să genereze cod mai eficient și astfel să accelereze execuția programului.

  • modularitatea;

Mecanismul de modularitate vă permite să împărțiți programele în mai multe părți (module) relativ independente, cu conexiuni clar definite între ele. Acest lucru simplifică procesul de proiectare și asistența ulterioară pentru mari sisteme software. Suportul pentru modularitate nu este o proprietate a limbajelor de programare funcționale, dar este acceptat de majoritatea acestor limbaje.

  • funcțiile sunt obiecte de calcul;

În limbajele funcționale (precum și în limbajele de programare și în matematică în general), funcțiile pot fi transmise altor funcții ca argument sau returnate ca rezultat. Funcțiile care iau argumente de funcție sunt numite funcții de ordin superior sau funcționale.

În programarea pură funcțională nu există operator de atribuire, obiectele nu pot fi modificate sau distruse, altele noi pot fi create doar prin descompunerea și sintetizarea celor existente. Colectorul de gunoi încorporat în limbaj va avea grijă de obiectele inutile. Datorită acestui fapt, în limbaje pur funcționale, toate funcțiile sunt lipsite de efecte secundare.

  • calcule amânate (leneși).

În limbajele tradiționale de programare (cum ar fi C++), apelarea unei funcții face ca toate argumentele să fie evaluate. Această metodă de apelare a unei funcții se numește apel după valoare. Dacă nu a fost folosit niciun argument în funcție, atunci rezultatul calculului se pierde, prin urmare, calculul a fost făcut în zadar. Într-un fel, opusul call-by-value este call-by-need (evaluare leneșă). În acest caz, argumentul este evaluat numai dacă este necesar pentru a calcula rezultatul.

Unele limbaje de programare funcționale

  • Gofel
  • MLWorks al lui Harlequin
  • Clasificarea limbajelor funcționale

    Ca exemplu pur Un limbaj funcțional poate fi dat de Haskell. Cu toate acestea, majoritatea limbilor funcționale sunt hibridși conțin proprietăți ale limbajelor atât funcționale, cât și imperative. Exemple vii sunt limbile Scala și Nemerle. Ele combină organic caracteristicile limbajelor orientate pe obiecte și funcționale. Recursiunea cozii și optimizarea acesteia sunt implementate; funcția este un obiect cu drepturi depline, adică poate fi stocată într-o variabilă, transmisă ca argument unei alte funcții sau returnată dintr-o funcție.

    Limbile funcționale sunt, de asemenea, împărțite în strictȘi lax. Limbile non-strictive le includ pe cele care acceptă evaluarea leneșă (F#), adică argumentele funcției sunt evaluate numai atunci când sunt necesare efectiv atunci când se evaluează funcția. Un exemplu izbitor de limbaj non-strict este Haskell. Un exemplu de limbaj strict este Standard ML.

    Unele limbaje funcționale sunt implementate pe deasupra mașinilor virtuale care formează platforme (JVM, .NET), adică aplicațiile în aceste limbi pot rula într-un mediu de rulare (JRE, CLR) și pot folosi clase încorporate. Acestea includ Scala, Clojure (JVM), F#, Nemerle, SML.NET (.NET).

    Legături

    • http://fprog.ru/ - Revista „Practica programării funcționale”
    • http://www.intuit.ru/department/pl/funcpl/1/ - Fundamentele programării funcționale. L. V. Gorodnyaya
    • http://roman-dushkin.narod.ru/fp.html - Un curs de prelegeri despre programare funcțională, susținut la MEPhI din 2001;
    • http://alexott.net/ru/fp/books/ - Revizuire a literaturii despre programarea funcțională. Sunt luate în considerare cărțile atât în ​​rusă, cât și în engleză.

    Fundația Wikimedia. 2010.

    Vedeți ce este „Limbajul de programare funcțional” în alte dicționare:

      Limbajul de programare Lisp- Limbajul de programare functional. Subiecte tehnologia de informațieîn general EN Lisp... Ghidul tehnic al traducătorului

      Un limbaj de programare universal de nivel înalt. Limbajul Lisp: se referă la limbaje declarative de tip funcțional; concepute pentru prelucrarea datelor de caractere prezentate sub formă de liste. Baza limbajului sunt funcțiile și recursive... ... Dicţionar financiar

      Acest termen are alte semnificații, vezi Alice. Alice Semantics: funcțional Tip de execuție: compilare la bytecode pentru mașină virtuală Apărut în: 2002 ... Wikipedia

      Acest termen are alte semnificații, vezi Scala. Scala Clasă de limbă: Multi-paradigma: func... Wikipedia

      Oz Semantică: funcțional, procedural, declarativ, orientat pe obiect, calcul constrâns, modele H, calcul paralel Tip de execuție: compilat Apărut în: 1991 Autor(i): Gert Smolka studenții săi Lansare... Wikipedia

      AWL (Alternative Web Language) Clasa de limbaj: multi-paradigmă: funcțional, procedural, orientat pe obiecte Tip de execuție: interpretat Apărut în: 2005 Tastarea datelor: dinamică ... Wikipedia

      Acest termen are alte semnificații, vezi Leda (sensuri). Leda este un limbaj de programare cu mai multe paradigme conceput de Timothy Budd. Limbajul Leda a fost creat inițial cu scopul de a combina programarea imperativă, bazată pe obiecte... ... Wikipedia

      Erlang Fișier:Erlang logo.png Semantică: multi-paradigmă: programare competitivă, funcțională Apărut în: 1987 Autor(i): Tastarea datelor: strict, dinamic Implementări principale: E ... Wikipedia

      În limbajele de programare funcționale, elementul de bază principal este conceptul matematic de funcție. Există diferențe în înțelegerea unei funcții în matematică și a unei funcții în programare, ca urmare a cărora C nu poate fi clasificat ca similar... ... Wikipedia

      Python a fost conceput în anii 1980 și crearea sa a început în decembrie 1989 de Guido van Rossum, ca parte a Centrului pentru Matematică și Informatică din Țările de Jos. Limbajul Python a fost conceput ca un descendent al limbajului de programare ABC, capabil să proceseze... ... Wikipedia

    Funcțiile sunt abstracții, în care detaliile de implementare ale unei acțiuni sunt ascunse în spatele unui nume separat. Un set bine scris de funcții le permite să fie folosite de mai multe ori. Biblioteca standard Python vine cu o multitudine de funcții pre-construite și depanate, dintre care multe sunt suficient de versatile pentru a gestiona o mare varietate de intrări. Chiar dacă o anumită secțiune de cod nu este folosită de mai multe ori, dar în ceea ce privește datele de intrare și ieșire este destul de autonomă, poate fi separată în siguranță într-o funcție separată.

    Această prelegere este mai orientată spre considerații practice decât spre teoria programării funcționale. Cu toate acestea, acolo unde este necesar, vor fi utilizați și explicați termeni relevanți.

    În continuare, ne vom uita în detaliu la descrierea și utilizarea funcțiilor în Python, recursiunea, funcțiile de trecere și returnare ca parametri, procesarea secvenței și iteratoare, precum și conceptul de generator. Se va demonstra că în Python, funcțiile sunt obiecte (și, prin urmare, pot fi trecute ca parametri și returnate ca rezultat al executării funcțiilor). În plus, vom vorbi despre cum puteți implementa unele mecanisme de programare funcțională care nu au suport sintactic direct în Python, dar sunt răspândite în limbaje de programare funcționale.

    Ce este programarea funcțională?

    Programare functionala este un stil de programare care folosește numai compoziţii de funcţii. Cu alte cuvinte, asta programareîn expresii mai degrabă decât în ​​comenzi imperative.

    După cum subliniază David Mertz în articolul său despre programarea funcțională în Python, „functional programare - programareîn limbaje funcționale (LISP, ML, OCAML, Haskell, ...)", ale căror principale atribute sunt:

    • „Prezența funcțiilor de primă clasă” (funcțiile, ca și alte obiecte, pot fi transferate în interiorul funcțiilor).
    • Recursiunea este principala structură de control dintr-un program.
    • Prelucrarea listelor (secvente).
    • Interzicerea efectelor secundare în funcții, ceea ce înseamnă în primul rând absența atribuirii (în limbaje funcționale „pure”)
    • Operatorii sunt interziși și se pune accent pe expresii. În loc de operatori, întregul program este în mod ideal o expresie cu definiții însoțitoare.
    • Întrebare cheie: Ce trebuie calculat, nu Cum.
    • Utilizarea funcțiilor de ordin superior (funcții peste funcții peste funcții).

    Program funcțional

    În matematică, o funcție afișează obiecte din același set ( seturi de definiții de funcție) altcuiva ( set de valori ale funcției). Funcții matematice (se numesc curat) „mecanic”, ei calculează fără ambiguitate rezultatul din argumentele date. Funcțiile pure nu ar trebui să stocheze date între două apeluri. Te poți gândi la ele ca la cutii negre, despre care știi doar ce fac, dar deloc cum.

    Programele de stil funcțional sunt concepute ca compoziţie funcții. În acest caz, funcțiile sunt înțelese aproape în același mod ca și în matematică: ele mapează un obiect cu altul. În programare, funcțiile „pure” sunt un ideal care nu este întotdeauna realizabil în practică. Practic caracteristici utile au de obicei prin efect: Salvați starea între apeluri sau modificați starea altor obiecte. De exemplu, este imposibil să ne imaginăm funcții I/O fără efecte secundare. De fapt, astfel de funcții sunt folosite de dragul acestor „efecte”. În plus, funcțiile matematice funcționează cu ușurință cu obiecte care necesită o cantitate infinită de informații (de exemplu, numere reale). În general, computerul

    Limbile similare cu cele funcționale folosesc un concept mai puțin strict. O funcție din matematică nu poate schimba mediul care o numește și își poate aminti rezultatele muncii sale, ci oferă doar rezultatul calculului funcției. Programarea folosind conceptul matematic al unei funcții provoacă unele dificultăți, astfel încât limbajele funcționale, într-o măsură sau alta, oferă și capacități imperative, ceea ce înrăutățește proiectarea programului (de exemplu, posibilitatea unor modificări ulterioare nedureroase). O diferență suplimentară față de limbajele de programare imperative este natura declarativă a descrierilor de funcții. Texte ale programelor în limbaje de programare funcționale descrie„cum se rezolvă o problemă”, dar nu prescrie succesiune de acțiuni pentru rezolvare. Primul limbaj funcțional conceput a fost Lisp. Opțiune a acestei limbi utilizat pe scară largă în sistemul de proiectare asistată de calculator AutoLISP

    Următoarele sunt de obicei considerate principalele proprietăți ale limbajelor de programare funcționale:

    • concizie și simplitate;
    • tastare puternică;
    • modularitatea;
    • funcțiile sunt obiecte de calcul;
    • calcule amânate (leneși).

    Unele limbaje de programare funcționale

  • Miranda (ce familie?)
  • Legături

    • http://roman-dushkin.narod.ru/fp.html - Un curs de prelegeri despre programare funcțională, susținut la MEPhI din 2001.

    Fundația Wikimedia. 2010.

    Vedeți ce este „Limbajul de programare funcțional” în alte dicționare:

      Un limbaj de programare care vă permite să specificați un program ca un set de definiții de funcție. În limbajele de programare funcționale: funcțiile fac schimb de date între ele fără a utiliza variabile și atribuiri intermediare;... ... Dicţionar financiar

      limbaj funcțional- Un limbaj de programare în care acțiunile asupra datelor sunt exprimate ca apeluri la proceduri funcționale. [GOST 19781 90] Subiecte de sprijin. sisteme de procesare informație software EN limbaj funcțional... Ghidul tehnic al traducătorului

      Ruby Semantică: multi-paradigmă Tip de execuție: interpret Apărut în: 1995 Autor(i): Yukihiro Matsumoto Ultima versiune: 1.9.1 ... Wikipedia

      Limbajul funcțional- 37. Limbajul funcțional Limbajul funcțional Un limbaj de programare în care acțiunile asupra datelor sunt exprimate sub formă de apeluri la proceduri funcționale Sursa: GOST 19781 90: Suport software pentru sistemele de procesare a informațiilor. Termeni si...... Dicționar-carte de referință de termeni ai documentației normative și tehnice

      Erlang Fișier:Erlang logo.png Semantică: multi-paradigmă: programare competitivă, funcțională Apărut în: 1987 Autor(i): Tastarea datelor: strict, dinamic Implementări principale: E ... Wikipedia

      Semantica schemei: funcțional Tip de execuție: interpret sau compilator Apărut în: 1970 Autor(i): Guy Steele și Gerald Sussman Tastarea datelor... Wikipedia

      Acest termen are alte semnificații, vezi Miranda. Miranda este un limbaj de programare funcțional creat în 1985 de David Turner ca limbaj funcțional standard. Are un sistem strict de tip polimorf, ...... Wikipedia

      Hope este un limbaj de programare funcțional dezvoltat la începutul anilor 1980; este predecesorul limbi Mirandași Haskell. Revista Byte, august 1985, a publicat pentru prima dată un ghid al limbajului Hope. Un exemplu de program de calcul... ... Wikipedia

      Acest termen are alte semnificații, vezi SASL. SASL este un limbaj de programare complet funcțional dezvoltat de David Turner la Universitatea St Andrews în 1972, bazat pe subsetul aplicativ al ISWIM. În 1976... ... Wikipedia

      Acest termen are alte semnificații, vezi Scala. Scala Clasă de limbă: Multi-paradigma: func... Wikipedia

    Cărți

    • Programare în Clojure. Practica folosirii Lisp-ului în lumea Java, Emerick Ch., Carper B., Grand K.. De ce mulți oameni aleg Clojure? Este un limbaj de programare funcțional care nu numai că vă permite să utilizați biblioteci Java, servicii și alte resurse JVM, dar concurează și cu...

    String reverse(String arg) ( if(arg.length == 0) ( return arg; ) else ( return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); ) )
    Această funcție este destul de lentă, deoarece se autoapelează în mod repetat. Este posibil să existe o scurgere de memorie aici, deoarece obiectele temporare sunt create de multe ori. Dar acesta este un stil funcțional. S-ar putea să vi se pare ciudat cum oamenii pot programa astfel. Ei bine, tocmai voiam să-ți spun.

    Beneficiile programării funcționale

    Probabil vă gândiți că nu pot face un argument pentru a justifica caracteristica monstruoasă de mai sus. Când am început să învăț programarea funcțională, așa credeam și eu. M-am înșelat. Există argumente foarte bune pentru acest stil. Unele dintre ele sunt subiective. De exemplu, programatorii susțin că programele funcționale sunt mai ușor de înțeles. Nu voi face astfel de argumente, pentru că toată lumea știe că ușurința de a înțelege este un lucru foarte subiectiv. Din fericire pentru mine, există încă o mulțime de argumente obiective.

    Testarea unitară

    Deoarece fiecare simbol din FP este imuabil, funcțiile nu au efecte secundare. Nu puteți modifica valorile variabilelor, iar o funcție nu poate schimba o valoare în afara domeniului său de aplicare și, prin urmare, nu poate afecta alte funcții (cum se poate întâmpla cu câmpurile de clasă sau variabilele globale). Aceasta înseamnă că singurul rezultat al execuției funcției este valoarea returnată. Și singurul lucru care poate afecta valoarea returnată sunt argumentele transmise funcției.

    Iată-l, visul albastru al testerilor de unitate. Puteți testa fiecare funcție dintr-un program folosind doar argumentele necesare. Nu este nevoie să apelați funcții în ordinea corectă sau să recreați starea externă corectă. Tot ce trebuie să faceți este să transmiteți argumente care se potrivesc cu cazurile marginale. Dacă toate funcțiile din programul dvs. trec testele unitare, atunci puteți fi mult mai încrezător în calitatea software-ului dvs. decât în ​​cazul limbajelor de programare imperative. În Java sau C++, verificarea valorii returnate nu este suficientă - funcția poate schimba starea externă, care este, de asemenea, supusă verificării. Nu există o astfel de problemă în FP.

    Depanare

    Dacă un program funcțional nu se comportă așa cum vă așteptați, atunci depanarea este o simplă simplă. Puteți reproduce întotdeauna problema deoarece eroarea din funcție nu depinde de cod străin care a fost executat anterior. Într-un program imperativ, eroarea apare doar pentru o perioadă. Va trebui să parcurgeți o serie de pași care nu au legătură cu bug-ul, din cauza faptului că funcția depinde de starea externăși efectele secundare ale altor funcții. În FP, situația este mult mai simplă - dacă valoarea returnată este incorectă, atunci va fi întotdeauna incorectă, indiferent ce bucăți de cod au fost executate înainte.

    Odată ce ați reprodus eroarea, găsiți sursa acesteia - sarcină banală. E chiar frumos. De îndată ce opriți programul, veți avea în față întreaga stivă de apeluri. Puteți vizualiza argumentele fiecărui apel de funcție, la fel ca într-un limbaj imperativ. Cu diferența că într-un program imperativ acest lucru nu este suficient, deoarece funcțiile depind de valorile câmpurilor, variabilelor globale și stărilor altor clase. O funcție în FP depinde doar de argumentele sale, iar această informație este chiar în fața ochilor tăi! Mai mult, într-un program imperativ, verificarea valorii returnate nu este suficientă pentru a spune dacă o bucată de cod se comportă corect. Va trebui să vânați zeci de obiecte în afara funcției pentru a vă asigura că totul funcționează corect. În programarea funcțională, tot ce trebuie să faci este să te uiți la valoarea returnată!

    Pe măsură ce parcurgeți stiva, acordați atenție argumentelor transmise și valorilor returnate. Odată ce valoarea returnată se abate de la normă, detaliați funcția și continuați. Acest lucru se repetă de mai multe ori până când găsiți sursa erorii!

    Multithreading

    Programul funcțional este imediat gata pentru paralelizare fără nicio modificare. Nu trebuie să vă faceți griji cu privire la blocaje sau condițiile de cursă, deoarece nu aveți nevoie de încuietori! Nici o singură bucată de date dintr-un program funcțional nu este schimbată de două ori de același fir sau de altele diferite. Acest lucru înseamnă că puteți adăuga cu ușurință fire de execuție la programul dvs. fără a fi vreodată să vă faceți griji cu privire la problemele inerente limbilor imperative.

    Dacă acesta este cazul, atunci de ce sunt atât de rar folosite limbajele de programare funcționale aplicații cu mai multe fire? De fapt, mai des decât crezi. Ericsson a dezvoltat un limbaj funcțional numit Erlang pentru utilizare pe comutatoarele de telecomunicații scalabile și tolerante la erori. Mulți au remarcat avantajele Erlang și au început să-l folosească. Vorbim despre telecomunicații și sisteme de control al traficului, care nu sunt nici pe departe la fel de ușor scalabile ca sistemele tipice dezvoltate pe Wall Street. De fapt, sistemele scrise în Erlang nu sunt la fel de scalabile și de încredere ca sistemele Java. Sistemele Erlang sunt pur și simplu super fiabile.

    Povestea multithreading-ului nu se termină aici. Dacă scrieți o aplicație în esență cu un singur thread, compilatorul poate optimiza în continuare programul funcțional pentru a utiliza mai multe procesoare. Să ne uităm la următoarea bucată de cod.


    Un compilator de limbaj funcțional poate analiza codul, clasifica funcțiile care produc liniile s1 și s2 ca funcții consumatoare de timp și le poate rula în paralel. Acest lucru este imposibil de realizat într-un limbaj imperativ, deoarece fiecare funcție poate schimba starea externă, iar codul imediat după apel poate depinde de aceasta. În FP, analiza automată a funcțiilor și căutarea candidaților potriviți pentru paralelizare este o sarcină banală, cum ar fi automat inline! În acest sens, stilul de programare funcțional este pregătit pentru viitor. Dezvoltatorii de hardware nu mai pot face CPU să funcționeze mai repede. În schimb, cresc numărul de nuclee și pretind o creștere de patru ori a vitezei de calcul cu mai multe fire. Desigur, ei uită să spună la timp că dvs procesor nou va prezenta o creștere doar a programelor dezvoltate ținând cont de paralelizare. Există foarte puține dintre acestea printre software-ul imperativ. Dar 100% programe functionale gata pentru multithreading din cutie.

    Implementare la cald

    Pe vremuri pentru a instala Actualizări Windows A trebuit să repornesc computerul. Multe ori. După instalarea unei noi versiuni a playerului media. Au existat schimbări semnificative în Windows XP, dar situația este încă departe de a fi ideală (am rulat Windows Update la serviciu astăzi și acum memento-ul enervant nu mă va lăsa în pace până nu repornesc). ÎN sisteme Unix modelul de actualizare a fost mai bun. Pentru a instala actualizări, a trebuit să opresc unele componente, dar nu întregul sistem de operare. Deși situația arată mai bine, dar pentru o clasă mare aplicații server acest lucru încă nu este acceptabil. Sistemele de telecomunicații trebuie să fie pornite 100% din timp, deoarece dacă o actualizare împiedică o persoană să cheme o ambulanță, se pot pierde vieți. De asemenea, firmele de pe Wall Street nu doresc să închidă serverele în weekend pentru a instala actualizări.

    În mod ideal, trebuie să actualizați toate secțiunile necesare ale codului fără a opri sistemul în principiu. În lumea imperativă acest lucru este imposibil [trad. în Smalltalk este foarte posibil]. Imaginați-vă că descărcați o clasă Java din mers și reîncărcați o nouă versiune. Dacă am face acest lucru, atunci toate instanțele clasei ar deveni nefuncționale, deoarece starea pe care au stocat-o s-ar pierde. Ar trebui să scriem un cod complicat pentru controlul versiunilor. Ar trebui să serializăm toate instanțele create ale clasei, apoi să le distrugem, să creăm instanțe ale unei clase noi, să încercăm să încărcăm datele serializate în speranța că migrarea va decurge fără probleme și noile instanțe vor fi valabile. Și în plus, codul de migrare trebuie scris manual de fiecare dată. Și codul de migrare trebuie să păstreze legăturile dintre obiecte. În teorie este în regulă, dar în practică nu va funcționa niciodată.

    Într-un program funcțional, toate stările sunt stocate pe stivă ca argumente ale funcției. Acest lucru face implementarea la cald mult mai ușoară! În esență, tot ce trebuie să faceți este să calculați diferența dintre codul de pe serverul de producție și noua versiune și să instalați modificările în cod. Restul se va face automat de instrumentele lingvistice! Dacă crezi că asta este science fiction, gândește-te de două ori. Inginerii care lucrează cu Erlang și-au actualizat sistemele de ani de zile fără a-și opri munca.

    Probe și optimizări asistate de mașini

    O altă proprietate interesantă a limbajelor de programare funcționale este că pot fi învățate din punct de vedere matematic. Deoarece un limbaj funcțional este o implementare a unui sistem formal, atunci totul operatii matematice folosit pe hârtie poate fi aplicat și la programele funcționale. Compilatorul, de exemplu, poate converti o bucată de cod într-o bucată echivalentă, dar mai eficientă, justificând în același timp matematic echivalența lor. Baze de date relaționale datele au fost supuse unor astfel de optimizări de ani de zile. Nimic nu te împiedică să folosești tehnici similareîn programele regulate.

    În plus, puteți folosi matematica pentru a demonstra corectitudinea secțiunilor programelor dvs. Dacă doriți, puteți scrie instrumente care să vă analizeze codul și să creați automat teste unitare pentru cazurile marginale! Această funcționalitate este de neprețuit pentru sistemele solide. Atunci când se dezvoltă sisteme de monitorizare a stimulatorului cardiac sau de management al traficului aerian, astfel de instrumente sunt obligatorii. Dacă dezvoltările dvs. nu sunt în domeniul aplicațiilor critice, atunci instrumentele verificare automată vă va oferi în continuare un avantaj gigantic față de concurenții dvs.

    Funcții de ordin superior

    Amintiți-vă, când am vorbit despre beneficiile FP, am observat că „totul arată frumos, dar este inutil dacă trebuie să scriu într-un limbaj stângaci în care totul este definitiv”. Aceasta a fost o concepție greșită. Utilizarea finalului peste tot pare stângace doar în limbaje de programare imperative, cum ar fi Java. Limbajele de programare funcționale operează cu alte tipuri de abstracții, care te fac să uiți că ți-a plăcut vreodată să schimbi variabile. Un astfel de instrument este funcțiile de ordin superior.

    În FP, o funcție nu este același lucru cu o funcție în Java sau C. Este un superset - ei pot face același lucru ca funcțiile Java și chiar mai mult. Să presupunem că avem o funcție în C:

    Int add(int i, int j) ( return i + j; )
    În FP, aceasta nu este același lucru cu o funcție C obișnuită. Să extindem compilatorul nostru Java pentru a sprijini această notație. Compilatorul trebuie să transforme declarația funcției în următorul cod Java (rețineți că există o final implicită peste tot):

    Clasa add_function_t ( int add(int i, int j) ( return i + j; ) ) add_function_t add = new add_function_t();
    Simbolul de adăugare nu este cu adevărat o funcție. Aceasta este o clasă mică cu o singură metodă. Acum putem trece add ca argument altor funcții. O putem scrie într-un alt simbol. Putem crea instanțe de add_function_t în timpul execuției și acestea vor fi distruse de colectorul de gunoi dacă nu mai sunt necesare. Funcțiile devin obiecte de bază, la fel ca numerele și șirurile de caractere. Funcțiile care operează pe funcții (luați-le drept argumente) sunt numite funcții de ordin superior. Nu lăsa asta să te sperie. Conceptul de funcții de ordin superior nu este aproape deloc diferit de Concepte Java clase care operează una pe cealaltă (putem trece clase la alte clase). Le putem numi „clase de ordin superior”, dar nimeni nu se deranjează cu asta, deoarece Java nu are o comunitate academică riguroasă în spate.

    Cum și când ar trebui să utilizați funcțiile de ordin superior? Mă bucur că ai întrebat. Îți scrii programul ca o bucată mare de cod monolitică fără să-ți faci griji cu privire la ierarhia claselor. Dacă vedeți că o bucată de cod se repetă în locuri diferite, o mutați într-o funcție separată (din fericire, școlile încă învață cum să facă acest lucru). Dacă observați că o parte din logica din funcția dvs. ar trebui să se comporte diferit în unele situații, atunci creați o funcție de ordin superior. Confuz? Aici exemplu real din munca mea.

    Să presupunem că avem o bucată de cod Java care primește un mesaj, îl transformă căi diferiteși îl transferă pe alt server.

    Void handleMessage(Msg de mesaj) ( // ... msg.setClientCode("ABCD_123"); // ... sendMessage(msg); ) // ... )
    Acum imaginați-vă că sistemul s-a schimbat și acum trebuie să distribuiți mesajele între două servere în loc de unul. Totul rămâne neschimbat, cu excepția codului client - al doilea server dorește să primească acest cod într-un format diferit. Cum putem face față acestei situații? Putem verifica unde ar trebui să meargă mesajul și putem seta codul client corect pe baza acestuia. De exemplu astfel:

    Clasa MessageHandler ( void handleMessage(Message msg) ( // ... if(msg.getDestination().equals("server1") ( msg.setClientCode("ABCD_123"); ) else ( msg.setClientCode("123_ABC") ; ) // ... trimiteMesaj(msg); ) // ... )
    Dar această abordare nu se extinde bine. Pe măsură ce se adaugă noi servere, caracteristica va crește liniar și efectuarea modificărilor va deveni un coșmar. Abordarea orientată pe obiect constă în izolarea unei superclase comune MessageHandler și subclasarea logicii pentru definirea codului client:

    Clasa abstractă MessageHandler ( void handleMessage(Message msg) ( // ... msg.setClientCode(getClientCode()); // ... sendMessage(msg); ) abstract String getClientCode(); // ... ) clasa MessageHandlerOne extinde MessageHandler ( String getClientCode() ( returnează „ABCD_123”; ) ) clasa MessageHandlerTwo extinde MessageHandler ( String getClientCode() ( returnează „123_ABCD”; ) )
    Acum pentru fiecare server putem crea o instanță a clasei corespunzătoare. Adăugarea de noi servere devine mai convenabilă. Dar pentru asta mica schimbare prea mult text. A trebuit să creez două tipuri noi doar pentru a adăuga suport pentru cod de client diferit! Acum să facem același lucru în limba noastră cu suport pentru funcții de ordin superior:

    Clasa MessageHandler ( void handleMessage(Message msg, Function getClientCode) ( // ... Message msg1 = msg.setClientCode(getClientCode()); // ... sendMessage(msg1); ) // ... ) String getClientCodeOne( ) ( returnează „ABCD_123”; ) String getClientCodeTwo() ( returnează „123_ABCD”; ) MessageHandler handler = new MessageHandler(); handler.handleMessage(someMsg, getClientCodeOne);
    Nu am creat noi tipuri și nu am complicat ierarhia claselor. Pur și simplu am trecut funcția ca parametru. Am obținut același efect ca și în omologul orientat pe obiecte, dar cu unele avantaje. Nu ne-am legat de nicio ierarhie de clasă: putem trece orice alte funcții la runtime și le putem modifica oricând, menținând în același timp un nivel ridicat de modularitate cu mai puțin cod. În esență, compilatorul a creat lipici orientat pe obiecte pentru noi! În același timp, toate celelalte avantaje ale FP sunt păstrate. Desigur, abstracțiile oferite de limbajele funcționale nu se termină aici. Funcțiile de ordin superior sunt doar începutul

    curry

    Majoritatea oamenilor pe care i-am întâlnit au citit cartea Design Patterns by Gang of Four. Orice programator care se respectă va spune că cartea nu este legată de niciun limbaj de programare specific, iar tiparele sunt aplicabile dezvoltării software în general. Aceasta este o afirmație nobilă. Dar, din păcate, este departe de adevăr.

    Limbile funcționale sunt incredibil de expresive. Într-un limbaj funcțional, nu veți avea nevoie de modele de design, deoarece limbajul este atât de înalt încât puteți începe cu ușurință programarea în concepte care elimină toate modelele de programare cunoscute. Unul dintre aceste modele este Adaptor (cu ce este diferit de Facade? Se pare că cineva trebuie să ștampileze). mai multe pagini pentru a îndeplini termenii contractului). Acest model se dovedește a fi inutil dacă limba acceptă curry.

    Modelul Adaptor este cel mai adesea aplicat unității „standard” de abstractizare din Java - clasa. În limbajele funcționale, modelul este aplicat funcțiilor. Modelul ia o interfață și o transformă într-o altă interfață în funcție de anumite cerințe. Iată un exemplu de model de adaptor:

    Int pow(int i, int j); int pătrat(int i) ( return pow(i, 2); )
    Acest cod adaptează interfața unei funcții care ridică un număr la o putere arbitrară la interfața unei funcții care pune în pătrat un număr. În cercurile academice asta cea mai simplă tehnică se numește curry (după logicianul Haskell Curry, care a efectuat o serie de trucuri matematice pentru a formaliza totul). Deoarece funcțiile sunt folosite peste tot ca argumente în FP, currying-ul este folosit foarte des pentru a aduce funcții la interfața necesară într-un loc sau altul. Deoarece interfața unei funcții este argumentele sale, currying este folosit pentru a reduce numărul de argumente (ca în exemplul de mai sus).

    Acest instrument este construit în limbaje funcționale. Nu trebuie să creați manual o funcție care include originalul. Un limbaj funcțional va face totul pentru tine. Ca de obicei, să ne extindem limba adăugând curry.

    Pătrat = int pow(int i, 2);
    Cu această linie creăm automat o funcție de pătrat cu un singur argument. Noua funcție va apela funcția pow, înlocuind 2 ca al doilea argument. Din perspectiva Java, ar arăta astfel:

    Clasa square_function_t ( int square(int i) ( return pow(i, 2); ) ) square_function_t square = new square_function_t();
    După cum puteți vedea, am scris pur și simplu un wrapper peste funcția originală. În FP, curry este doar o modalitate simplă și convenabilă de a crea ambalaje. Te concentrezi pe sarcină și compilatorul scrie codul necesar Pentru dumneavoastră! Este foarte simplu și se întâmplă de fiecare dată când doriți să utilizați modelul Adaptor (wrapper).

    Evaluare leneșă

    Evaluarea leneșă (sau amânată) este o tehnică interesantă care devine posibilă odată ce înțelegi filozofia funcțională. Am văzut deja următoarea bucată de cod când vorbim despre multithreading:

    String s1 = somewhatLongOperation1(); String s2 = somewhatLongOperation2(); String s3 = concatenate(s1, s2);
    În limbajele de programare imperative, ordinea calculului nu ridică întrebări. Deoarece fiecare funcție poate afecta sau depinde de starea externă, este necesar să se mențină o ordine clară a apelurilor: mai întâi somewhatLongOperation1 , apoi somewhatLongOperation2 și concatenați la sfârșit. Dar nu totul este atât de simplu în limbaje funcționale.

    După cum am văzut mai devreme, somewhatLongOperation1 și somewhatLongOperation2 pot fi rulate simultan, deoarece funcțiile sunt garantate să nu afecteze sau să depind de starea globală. Dar dacă nu vrem să le executăm simultan, ar trebui să le numim secvenţial? Raspunsul este nu. Aceste calcule ar trebui să fie executate numai dacă orice altă funcție depinde de s1 și s2. Nici măcar nu trebuie să le executăm până nu avem nevoie de ele în concatenate . Dacă în loc să concatenăm înlocuim o funcție care, în funcție de condiție, folosește unul dintre cele două argumente, atunci al doilea argument poate să nu fie calculat nici măcar! Haskell este un exemplu de limbaj de evaluare leneș. Haskell nu garantează niciun ordin de apel (deloc!), deoarece Haskell execută cod după cum este necesar.

    Calculul leneș are o serie de avantaje, precum și unele dezavantaje. În secțiunea următoare vom discuta despre avantaje și vă voi explica cum să trăiți cu dezavantajele.

    Optimizare

    Evaluarea leneșă oferă un potențial enorm de optimizare. Un compilator leneș privește codul exact ca un matematician studiază expresii algebrice- poate anula anumite lucruri, anula execuția anumitor secțiuni de cod, poate schimba ordinea apelurilor pentru o mai mare eficiență, chiar poate aranja codul în așa fel încât să reducă numărul de erori, garantând în același timp integritatea programului. Exact asta mare avantaj atunci când descrieți un program folosind primitive formale stricte, codul se supune legi matematiceși poate fi studiat metode matematice.

    Abstractizarea structurilor de control

    Calculul leneș oferă un nivel atât de ridicat de abstractizare încât devin posibile lucruri uimitoare. De exemplu, imaginați-vă că implementați următoarea structură de control:

    Cu excepția cazului în care(stocul.isEuropean()) ( trimitToSEC(stoc); )
    Dorim ca funcția sendToSEC să fie executată doar dacă stocul nu este european. Cum poți implementa dacă nu? Fără o evaluare leneșă, am avea nevoie de un sistem macro, dar în limbi precum Haskell acest lucru nu este necesar. Putem declara dacă nu ca funcție!

    Nulă, cu excepția cazului în care (condiție booleană, cod Listă) ( dacă (! condiție) cod; )
    Rețineți că codul nu va fi executat dacă condiția == true . În limbile stricte, acest comportament nu poate fi repetat deoarece argumentele vor fi evaluate înainte, dacă nu sunt apelate.

    Structuri infinite de date

    Limbile leneșe vă permit să creați structuri de date infinite, care sunt mult mai dificil de creat în limbi stricte. - doar nu în Python]. De exemplu, imaginați-vă șirul lui Fibonacci. Evident, nu putem calcula o listă infinită într-un timp finit și totuși să o stocăm în memorie. În limbaje stricte precum Java, am scrie pur și simplu o funcție care returnează un membru arbitrar al secvenței. În limbi precum Haskell, putem abstrage și pur și simplu declara o listă infinită de numere Fibonacci. Deoarece limbajul este leneș, vor fi calculate doar părțile necesare ale listei care sunt utilizate efectiv în program. Acest lucru vă permite să faceți abstracție dintr-un număr mare de probleme și să le priviți de la un nivel superior (de exemplu, puteți utiliza funcții pentru procesarea listelor pe secvențe infinite).

    Defecte

    Cu siguranță brânză gratuită se întâmplă doar într-o capcană pentru șoareci. Calculele leneși vin cu o serie de dezavantaje. Acestea sunt în principal neajunsuri ale lenei. În realitate, ordinea directă a calculelor este foarte des necesară. Luați de exemplu următorul cod:


    Într-un limbaj leneș, nimeni nu garantează că prima linie va fi executată înainte de a doua! Aceasta înseamnă că nu putem face I/O, nu putem folosi funcțiile native în mod normal (la urma urmei, acestea trebuie să fie apelate în într-o anumită ordine pentru a lua în considerare efectele lor secundare) și nu pot interacționa cu lumea de afara! Dacă introducem un mecanism de ordonare a execuției codului, vom pierde avantajul rigoarei matematice a codului (și atunci vom pierde toate beneficiile programării funcționale). Din fericire, nu totul este pierdut. Matematicienii s-au pus pe treabă și au venit cu mai multe tehnici pentru a se asigura că instrucțiunile au fost executate în ordinea corectă, fără a pierde spiritul funcțional. Avem ce este mai bun din ambele lumi! Astfel de tehnici includ continuări, monade și scrierea unicității. În acest articol vom lucra cu continuări și vom lăsa monade și tastarea fără ambiguitate până data viitoare. Este interesant că continuările sunt un lucru foarte util, care este folosit nu numai pentru a specifica o ordine strictă a calculelor. Vom vorbi și despre asta.

    Continuări

    Continuările în programare joacă același rol ca Codul lui Da Vinci în istoria omenirii: o revelație surprinzătoare a celui mai mare mister al umanității. Ei bine, poate nu chiar așa, dar cu siguranță smulg copertele, așa cum ați învățat să luați rădăcina lui -1 în timpul zilei.

    Când ne-am uitat la funcții, am aflat doar jumătate din adevăr, deoarece am presupus că o funcție returnează o valoare funcției care o apelează. În acest sens, continuarea este o generalizare a funcțiilor. O funcție nu trebuie să returneze controlul în locul din care a fost apelată, dar poate reveni în orice loc din program. „Continuare” este un parametru pe care îl putem transmite unei funcții pentru a indica un punct de întoarcere. Sună mult mai înfricoșător decât este de fapt. Să aruncăm o privire la următorul cod:

    Int i = add(5, 10); int j = pătrat(i);
    Funcția de adăugare returnează numărul 15, care este scris în i în locația în care a fost apelată funcția. Valoarea lui i este folosită atunci când se apelează la pătrat. Rețineți că un compilator leneș nu poate schimba ordinea calculelor, deoarece a doua linie depinde de rezultatul primei. Putem rescrie acest cod folosind un stil de trecere de continuare (CPS), unde add returnează o valoare funcției pătrate.

    Int j = add(5, 10, square);
    În acest caz, add primește un argument suplimentar - o funcție care va fi apelată după ce add se termină de rulat. În ambele exemple, j va fi egal cu 225.

    Aceasta este prima tehnică care vă permite să specificați ordinea în care sunt executate două expresii. Să revenim la exemplul nostru I/O.

    System.out.println("Vă rugăm să introduceți numele dvs.: "); System.in.readLine();
    Aceste două linii sunt independente una de cealaltă, iar compilatorul este liber să-și schimbe ordinea după cum dorește. Dar dacă îl rescriem în CPS, vom adăuga astfel dependența necesară, iar compilatorul va trebui să efectueze calcule unul după altul!

    System.out.println("Vă rugăm să introduceți numele dvs.: ", System.in.readLine);
    În acest caz, println ar trebui să apeleze readLine , trecându-i rezultatul și să returneze rezultatul readLine la sfârșit. În această formă, putem fi siguri că aceste funcții vor fi apelate pe rând și că readLine va fi apelată deloc (la urma urmei, compilatorul se așteaptă să primească rezultatul ultimei operații). În cazul Java, println returnează void. Dar dacă ar fi returnată o valoare abstractă (care ar putea servi drept argument pentru readLine), asta ar rezolva problema noastră! Desigur, construirea unor astfel de lanțuri de funcții afectează foarte mult lizibilitatea codului, dar acest lucru poate fi rezolvat. Putem adăuga în limbajul nostru caracteristici sintactice care ne vor permite să scriem expresii ca de obicei, iar compilatorul va înlănțui automat calculele. Acum putem efectua calcule în orice ordine fără a pierde avantajele FP (inclusiv capacitatea de a studia programul folosind metode matematice)! Dacă acest lucru este confuz, amintiți-vă că funcțiile sunt doar instanțe ale unei clase cu un singur membru. Rescrieți exemplul nostru astfel încât println și readLine să fie instanțe ale claselor, acest lucru vă va face mai clar.

    Dar beneficiile sequelelor nu se termină aici. Putem scrie întregul program folosind CPS, astfel încât fiecare funcție să fie apelată cu parametru suplimentar, o continuare în care se trece rezultatul. În principiu, orice program poate fi tradus în CPS dacă fiecare funcție este tratată ca un caz special de continuare. Această conversie se poate face automat (de fapt, mulți compilatori fac acest lucru).

    De îndată ce convertim programul în formă CPS, devine clar că fiecare instrucțiune are o continuare, funcție căreia i se va trece rezultatul, care într-un program normal ar fi punctul de apel. Să luăm orice instrucțiune din ultimul exemplu, de exemplu add(5,10) . Într-un program scris în formă CPS, este clar care va fi continuarea - aceasta este funcția pe care add o va apela la finalizarea lucrării. Dar care va fi continuarea în cazul unui program non-CPS? Desigur, putem converti programul în CPS, dar este necesar?

    Se pare că acest lucru nu este necesar. Aruncă o privire atentă la conversia noastră CPS. Dacă începeți să scrieți un compilator pentru acesta, veți descoperi că versiunea CPS nu are nevoie de o stivă! Funcțiile nu returnează niciodată nimic, în sensul tradițional al cuvântului „întoarcere”, ele numesc pur și simplu o altă funcție, înlocuind rezultatul calculului. Nu este nevoie să introduceți argumente în stivă înainte de fiecare apel și apoi să le faceți înapoi. Putem stoca pur și simplu argumentele într-o locație de memorie fixă ​​și să folosim jump în loc de un apel normal. Nu trebuie să stocăm argumentele originale, pentru că nu vor mai fi necesare niciodată, deoarece funcțiile nu returnează nimic!

    Astfel, programele în stil CPS nu au nevoie de stivă, ci conțin un argument suplimentar, sub forma unei funcții, pentru a fi apelat. Programele în stil non-CPS nu au un argument suplimentar, dar folosesc o stivă. Ce este stocat pe stivă? Doar argumente și un pointer către locația de memorie unde ar trebui să revină funcția. Ei bine, ai ghicit deja? Stiva stochează informații despre continuări! Un pointer către un punct de întoarcere pe stivă este același cu funcția care trebuie apelată în programele CPS! Pentru a afla care este continuarea lui add(5,10), trebuie doar să luați punctul de întoarcere din stivă.

    Nu a fost greu. O continuare și un pointer către un punct de întoarcere sunt de fapt același lucru, doar continuarea este specificată în mod explicit și, prin urmare, poate diferi de locul în care a fost apelată funcția. Dacă vă amintiți că o continuare este o funcție, iar o funcție în limba noastră este compilată într-o instanță a unei clase, atunci veți înțelege că un pointer către un punct de întoarcere din stivă și un pointer către o continuare sunt de fapt același lucru , deoarece funcția noastră (ca o instanță a unei clase) este doar un pointer. Aceasta înseamnă că în orice moment al programului dumneavoastră puteți solicita continuarea curentă (în esență informații din stivă).

    Bine, acum înțelegem care este continuarea curentă. Ce înseamnă? Dacă luăm continuarea curentă și o salvăm undeva, salvăm astfel starea curentă a programului - o înghețăm. Acesta este similar cu modul de hibernare a sistemului de operare. Obiectul de continuare stochează informațiile necesare pentru a relua execuția programului din punctul în care a fost solicitat obiectul de continuare. sistem de operare face acest lucru programelor dvs. tot timpul când schimbă contextul între fire. Singura diferență este că totul este sub controlul sistemului de operare. Dacă solicitați un obiect de continuare (în Scheme acest lucru se face apelând funcția call-with-current-continuation), atunci veți primi un obiect cu continuarea curentă - stiva (sau în cazul CPS, următoarea funcție de apelare ). Puteți salva acest obiect pe o variabilă (sau chiar pe disc). Dacă decideți să „reporniți” programul cu această continuare, atunci starea programului dumneavoastră este „transformată” la starea la momentul în care obiectul de continuare a fost preluat. Este același lucru cu trecerea la un fir suspendat sau cu trezirea sistemului de operare după hibernare. Cu excepția faptului că poți face asta de mai multe ori la rând. După ce sistemul de operare se trezește, informațiile de hibernare sunt distruse. Dacă acest lucru nu s-a făcut, atunci ar fi posibil să se restabilească starea sistemului de operare din același punct. Este aproape ca și cum ai călători în timp. Cu sequelele îți poți permite!

    În ce situații vor fi utile continuarea? De obicei, dacă încercați să emulați starea în sisteme care sunt în mod inerent lipsite de stare. O utilizare excelentă pentru continuare a fost găsită în aplicațiile Web (de exemplu, în cadrul Seaside pentru limbajul Smalltalk). ASP.NET de la Microsoft face tot posibilul pentru a salva starea dintre solicitări pentru a vă ușura viața. Dacă C# acceptă continuări, complexitatea ASP.NET ar putea fi redusă la jumătate prin simpla salvare a continuării și restabilirea acesteia la următoarea solicitare. Din punctul de vedere al unui programator Web, nu ar exista o singură pauză - programul și-ar continua munca de la următoarea linie! Continuările sunt o abstractizare incredibil de utilă pentru rezolvarea unor probleme. Cu tot mai mulți clienți grasi tradiționali care se mută pe Web, importanța continuărilor va crește doar în timp.

    Potrivire de model

    Potrivirea modelelor nu este o idee atât de nouă sau inovatoare. De fapt, are puțină legătură cu programarea funcțională. Singurul motiv pentru care este adesea asociat cu FP este că de ceva timp limbile funcționale au potrivire de modele, dar limbajele imperative nu.

    Să începem introducerea noastră în potrivirea modelelor cu următorul exemplu. Iată o funcție pentru calcularea numerelor Fibonacci în Java:

    Int fib(int n) ( dacă (n == 0) returnează 1; dacă (n == 1) returnează 1; returnează fib(n - 2) + fib (n - 1); )
    Și iată un exemplu într-un limbaj asemănător Java cu suport pentru potrivirea modelelor

    Int fib(0) ( return 1; ) int fib(1) ( return 1; ) int fib(int n) ( return fib(n - 2) + fib(n - 1); )
    Care este diferența? Compilatorul implementează ramificarea pentru noi.

    Gândește-te, este foarte important! Chiar nu este atât de important. Sa observat că un număr mare de funcții conțin structuri de comutare complexe (acest lucru este parțial adevărat pentru programele funcționale) și s-a decis să evidențiem acest punct. Definiția funcției este împărțită în mai multe variante și se stabilește un model în locul argumentelor funcției (aceasta amintește de supraîncărcarea metodei). Când are loc un apel de funcție, compilatorul compară argumentele cu toate definițiile din mers și o selectează pe cea mai potrivită. De obicei, alegerea cade pe cea mai specializată definiție a funcției. De exemplu, int fib(int n) poate fi apelat când n este 1, dar nu va fi, deoarece int fib(1) este o definiție mai specializată.

    Potrivirea modelelor pare de obicei mai complicată decât în ​​exemplul nostru. De exemplu un sistem complex Potrivirea modelelor vă permite să scrieți următorul cod:

    Int f(int n< 10) { ... } int f(int n) { ... }
    Când este utilă potrivirea modelelor? Lista acestor cazuri este surprinzător de lungă! De fiecare dată când utilizați constructe complexe imbricate if, potrivirea modelelor poate face o treabă mai bună cu mai puțin cod. Un bun exemplu care îmi vine în minte este funcția WndProc, care este implementată în fiecare program Win32 (chiar dacă este ascunsă de programator în spatele unui gard înalt de abstracții). De obicei, potrivirea modelelor poate verifica chiar și conținutul colecțiilor. De exemplu, dacă transmiteți o matrice unei funcții, atunci puteți selecta toate matricele al căror prim element este egal cu 1 și al căror al treilea element este mai mare de 3.

    Un alt avantaj al potrivirii modelelor este că, dacă faceți modificări, nu va trebui să căutați o singură funcție uriașă. Tot ce trebuie să faceți este să adăugați (sau să modificați) unele definiții ale funcției. Astfel, scăpăm de un întreg strat de modele din celebra carte a Gang of Four. Cu cât condițiile sunt mai complexe și mai ramificate, cu atât va fi mai util să folosiți potrivirea modelelor. Odată ce începi să le folosești, te vei întreba cum te-ai descurcat vreodată fără ele.

    Închideri

    Până acum, am discutat despre caracteristicile FP în contextul limbajelor „pur” funcționale - limbaje care sunt implementări ale calculului lambda și nu conțin caracteristici care contrazic sistemul formal al Bisericii. Cu toate acestea, multe caracteristici ale limbajelor funcționale sunt utilizate în afara calculului lambda. Deși implementarea unui sistem axiomatic este interesantă din punct de vedere al programării din punct de vedere al expresiilor matematice, aceasta poate să nu fie întotdeauna aplicabilă în practică. Multe limbi preferă să folosească elemente ale limbilor funcționale fără a adera la o doctrină funcțională strictă. Unele astfel de limbi (de exemplu Common Lisp) nu necesită ca variabilele să fie definitive - valorile acestora pot fi modificate. Nici măcar nu necesită ca funcțiile să depindă doar de argumentele lor – funcțiilor li se permite să acceseze starea în afara domeniului lor. Dar, în același timp, includ caracteristici precum funcții de ordin superior. Trecerea unei funcții într-un limbaj non-pur este ușor diferită de aceeași operație în calculul lambda și necesită prezența caracteristică interesantă numită: închidere lexicală. Să aruncăm o privire la următorul exemplu. Ține minte că în în acest caz, variabilele nu sunt finale și o funcție poate accesa variabile în afara domeniului său de aplicare:

    Funcția makePowerFn(int putere) ( int powerFn(int bază) ( return pow(bază, putere); ) return powerFn; ) Funcție pătrat = makePowerFn(2); pătrat(3); // returnează 9
    Funcția make-power-fn returnează o funcție care ia un argument și îl ridică la o anumită putere. Ce se întâmplă când încercăm să evaluăm pătratul(3)? Puterea variabilă este în afara domeniului de aplicare a powerFn deoarece makePowerFn a fost deja finalizată și stiva sa a fost distrusă. Atunci cum funcționează pătratul? Limbajul trebuie să stocheze cumva sensul puterii pentru ca funcția pătrat să funcționeze. Ce se întâmplă dacă creăm o altă funcție cub care ridică un număr la a treia putere? Limbajul va trebui să stocheze două valori de putere pentru fiecare funcție creată în make-power-fn. Fenomenul de stocare a acestor valori se numește închidere. O închidere nu numai că păstrează argumentele funcției de sus. De exemplu, o închidere ar putea arăta astfel:

    Funcția makeIncrementer() ( int n = 0; int increment() ( return ++n; ) ) Funcția inc1 = makeIncrementer(); Funcția inc2 = makeIncrementer(); inc1(); // returnează 1; inc1(); // returnează 2; inc1(); // returnează 3; inc2(); // returnează 1; inc2(); // returnează 2; inc2(); // returnează 3;
    În timpul execuției, valorile lui n sunt stocate și contoarele au acces la ele. Mai mult, fiecare contor are propria copie a lui n, în ciuda faptului că ar fi trebuit să dispară după ce funcția makeIncrementer a rulat. Cum reușește compilatorul să compileze asta? Ce se întâmplă în culisele închiderilor? Din fericire, avem un permis magic.

    Totul se face destul de logic. La prima vedere, este clar că variabilele locale nu mai sunt supuse regulilor de aplicare și durata lor de viață este nedefinită. Evident, nu mai sunt depozitate pe stivă - trebuie păstrate pe grămadă. Închiderea se face deci ca functionare normala, despre care am discutat mai devreme, cu excepția faptului că are o referință suplimentară la variabilele din jur:

    Clasa some_function_t ( SymbolTable parentScope; // ... )
    Dacă o închidere accesează o variabilă care nu se află în domeniul local, atunci ia în considerare domeniul părinte. Asta e tot! Închiderea conectează lumea funcțională cu lumea OOP. De fiecare dată când creați o clasă care stochează o anumită stare și o treceți undeva, amintiți-vă despre închideri. O închidere este doar un obiect care creează „atribute” din mers, scoțându-le din sfera de aplicare, astfel încât să nu fie nevoie să o faci singur.

    Acum ce?

    Acest articol atinge doar vârful aisbergului de programare funcțională. Poți să sapi mai adânc și să vezi ceva cu adevărat mare și, în cazul nostru, ceva bun. În viitor am de gând să scriu despre teoria categoriilor, monade, structuri functionale date, sisteme de tip în limbaje funcționale, multithreading funcțional, bazele funcționale date și despre multe alte lucruri. Dacă pot să scriu (și să studiez în acest proces) chiar și jumătate din aceste subiecte, viața mea nu va fi în zadar. Între timp, Google- prietenul tău credincios.

    Dacă sunteți un dezvoltator ca mine, probabil că ați studiat mai întâi paradigma OOP. Primul tău limbaj a fost Java sau C++ - sau, dacă ai noroc, Ruby, Python sau C# - așa că probabil știi ce sunt clase, obiecte, instanțe etc. Ceea ce cu siguranță nu înțelegeți prea multe sunt elementele de bază ale acelei paradigme ciudate numite programare funcțională, care diferă semnificativ nu numai de OOP, ci și de programare procedurală, orientată pe prototip și alte tipuri de programare.

    Programarea funcțională devine populară - și din motive întemeiate. Paradigma în sine nu este nouă: Haskell este poate cel mai funcțional limbaj și a apărut în anii 90. Limbi precum Erlang, Scala, Clojure se încadrează și ele sub definiția funcționalului. Unul dintre principalele avantaje ale programării funcționale este capacitatea de a scrie programe care funcționează concomitent (dacă ați uitat deja ce este, reîmprospătați-vă memoria citind) și fără erori - adică blocarea reciprocă și siguranța firelor nu vă vor deranja. .

    Programarea funcțională are multe avantaje, dar capacitatea de a maximiza resursele CPU prin comportamentul concurent este principalul său avantaj. Mai jos ne vom uita la principiile de bază ale programării funcționale.

    Introducere: Toate aceste principii sunt opționale (multe limbi nu le respectă pe deplin). Toate sunt teoretice și sunt necesare cel mai mult definiție precisă paradigma functionala.

    1. Toate funcțiile sunt pure

    Această regulă este cu siguranță fundamentală în programarea funcțională. Toate funcțiile sunt pure dacă îndeplinesc două condiții:

    1. O funcție apelată cu aceleași argumente returnează întotdeauna aceeași valoare.
    2. Nu apar efecte secundare în timpul execuției funcției.

    Prima regulă este clară - dacă apelez la funcția sum(2, 3), mă aștept ca rezultatul să fie întotdeauna 5. De îndată ce apelați funcția rand() sau accesați o variabilă care nu este definită în funcție, puritatea funcției este încălcată, iar acest lucru nu este permis în programarea funcțională.

    A doua regulă - fără efecte secundare - este de natură mai largă. Un efect secundar este o modificare a altceva decât funcția care se execută în prezent. Schimbarea unei variabile în afara unei funcții, imprimarea pe consolă, aruncarea unei excepții, citirea datelor dintr-un fișier - toate acestea sunt exemple de efecte secundare care privează funcția de puritatea sa. Aceasta poate părea o limitare majoră, dar gândiți-vă din nou. Dacă sunteți sigur că apelarea unei funcții nu va schimba nimic „din exterior”, atunci puteți utiliza această funcție în orice scenariu. Acest lucru deschide ușa către programarea concomitentă și aplicațiile multi-threaded.

    2. Toate funcțiile sunt de primă clasă și de ordin superior

    Acest concept nu este o caracteristică a FP (este folosit în Javascript, PHP și alte limbi) - dar este o cerință obligatorie. De fapt, Wikipedia are un articol întreg dedicat funcțiilor de primă clasă. Pentru ca o funcție să fie de primă clasă, trebuie să poată fi declarată ca variabilă. Acest lucru permite ca funcția să fie tratată ca un tip de date normal și să fie în continuare executată.

    3. Variabilele sunt imuabile

    Totul este simplu aici. În programarea funcțională, nu puteți modifica o variabilă după ce a fost inițializată. Puteți crea altele noi, dar nu le puteți modifica pe cele existente - și datorită acestui lucru, puteți fi sigur că nicio variabilă nu se va schimba.

    4. Transparența relativă a funcțiilor

    Este dificil de dat o definiție corectă a transparenței relative. Cred că cel mai precis este acesta: dacă puteți înlocui un apel de funcție cu o valoare returnată, iar starea nu se schimbă, atunci funcția este relativ transparentă. Acest lucru poate fi evident, dar voi da un exemplu.

    Să presupunem că avem o funcție Java care adaugă 3 și 5:

    Public int addNumbers() ( returnează 3 + 5; ) addNumbers() // 8 8 // 8

    Evident, orice apel la această funcție poate fi înlocuit cu 8 - ceea ce înseamnă că funcția este relativ transparentă. Iată un exemplu de funcție opace:

    Public void printText())( System.out.println("Hello World"); ) printText() // Nu returnează nimic, dar afișează "Hello World"

    Această funcție nu returnează nimic, dar tipărește text, iar dacă înlocuiți apelul funcției cu nimic, starea consolei va fi diferită - ceea ce înseamnă că funcția nu este relativ transparentă.

    5. Programarea funcțională se bazează pe calculul lambda

    Programarea funcțională se bazează în mare măsură pe un sistem matematic numit calcul lambda. Nu sunt matematician, așa că nu voi intra în detalii - dar vreau să atrag atenția asupra două principii cheie ale calculului lambda care formează însuși conceptul de programare funcțională:

    1. În calculul lambda, toate funcțiile pot fi anonime, deoarece singura parte semnificativă a antetului funcției este lista de argumente.
    2. Când sunt apelate, toate funcțiile trec printr-un proces de curry. Este în felul următor: dacă este apelată o funcție cu mai multe argumente, atunci la început va fi executată doar cu primul argument și va returna o nouă funcție care conține 1 argument mai puțin, care va fi apelată imediat. Acest proces este recursiv și continuă până când toate argumentele au fost aplicate, returnând rezultatul final. Deoarece funcțiile sunt pure, aceasta funcționează.

    După cum am spus, calculul lambda nu se termină aici - dar am acoperit doar aspectele cheie legate de FP. Acum, într-o conversație despre programarea funcțională, puteți arunca cuvântul „calcul lambda” și toată lumea va crede că vă încurcați :)

    • Serghei Savenkov

      un fel de recenzie „scurtă”... de parcă s-ar grăbi undeva