Aplicații funcționale. Funcții de ordine superioară. Vedeți ce este „Limbajul de programare funcțional” în alte dicționare

Programarea funcțională combină diferite abordări pentru definirea proceselor de calcul bazate pe concepte abstracte destul de stricte și metode de prelucrare a datelor simbolice.

O caracteristică a limbajelor de programare funcționale este că textele programelor în limbaje de programare funcționale descriu „cum se rezolvă o problemă”, dar nu prescriu o secvență de acțiuni pentru rezolvarea acesteia. Proprietăți de bază limbaje funcționale programare: concizie, simplitate, tastare puternică, modularitate, prezența calculelor amânate (leneșe).

Limbajele de programare funcționale includ: Lisp, Miranda, Gofel, ML, Standard ML, Objective CAML, F#, Scala, Pythagoras etc.

Limbaje de programare procedurale

Un limbaj de programare procedural permite programatorului să definească fiecare pas în procesul de rezolvare a unei probleme. Particularitatea unor astfel de limbaje de programare este că sarcinile sunt împărțite în pași și rezolvate pas cu pas. Folosind un limbaj procedural, programatorul definește constructe de limbaj pentru a efectua o secvență de pași algoritmici.

Limbaje de programare procedurale: Ada, Basic, C, COBOL, Pascal, PL/1, Rapier etc.

Stiva limbaje de programare

Un limbaj de programare stivă este un limbaj de programare care utilizează modelul stivă de mașini pentru a transmite parametri. Limbaje de programare bazate pe stivă: Forth, PostScript, Java, C# etc. Când utilizați stiva ca canal principal pentru trecerea parametrilor între cuvinte, elementele de limbaj formează în mod natural fraze (înlănțuire secvențială). Această proprietate aduce aceste limbi mai aproape de limbile naturale.

Limbaje de programare orientate pe aspecte 5) Limbaje de programare declarative 6) Limbaje de programare dinamice 7) Limbaje de programare educaționale 8) Limbaje de descriere a interfeței 9) Limbaje de programare prototip 10) Limbaje de programare orientate pe obiecte ​11) Limbaje logice de programare 12) Limbaje de programare scripting 13) Limbaje de programare ezoterice


Standardizarea limbajelor de programare. Paradigma de programare

Conceptul de limbaj de programare este indisolubil legat de implementarea sa. Pentru a se asigura că compilarea aceluiași program de către diferiți compilatori dă întotdeauna același rezultat, sunt dezvoltate standarde de limbaj de programare. Organizații de standardizare: American National Standards Institute ANSI, Institute of Electrical and Electronics Engineers IEEE, ISO International Standards Organization.



Când o limbă este creată, este lansat un standard privat, determinat de dezvoltatorii limbii. Dacă o limbă devine răspândită, atunci în timp versiuni diferite compilatoare care nu respectă întocmai un standard privat. În cele mai multe cazuri, există o extindere a capabilităților fixate inițial ale limbii. Un standard de consens este în curs de dezvoltare pentru a aduce cele mai populare implementări ale limbajului în conformitate unele cu altele. Un factor foarte important în standardizarea unui limbaj de programare este oportunitatea apariției standardului - înainte ca limbajul să se răspândească și să fie create multe implementări incompatibile. În procesul dezvoltării limbajului, pot apărea noi standarde, reflectând inovațiile moderne.

Paradigme de programare

Paradigmă- un set de teorii, standarde și metode care împreună reprezintă un mod de organizare a cunoștințelor științifice - cu alte cuvinte, un mod de a vedea lumea. Prin analogie, este general acceptat că o paradigmă în programare este o modalitate de conceptualizare care definește modul în care trebuie efectuate calculele și modul în care ar trebui să fie structurată și organizată munca efectuată de un computer.

Există mai multe paradigme de programare de bază, dintre care cele mai importante în acest moment sunt paradigmele de programare directă, orientată pe obiecte și funcțional-logic. Pentru a sprijini programarea în conformitate cu o anumită paradigmă, au fost dezvoltate limbaje algoritmice speciale.

C și Pascal sunt exemple de limbaje concepute pentru programarea prescriptivă, în care dezvoltatorul de program utilizează un model orientat pe proces, adică încearcă să creeze cod care funcționează pe date în mod corespunzător. În această abordare, principiul activ este considerat a fi un program (cod), care trebuie să efectueze toate acțiunile necesare asupra datelor pasive pentru a obține rezultatul dorit.


Tehnologia de programare ca proces de dezvoltare a produselor software create ca un întreg inextricabil sub formă de programe bine testate și materiale metodologice care descriu scopul și utilizarea acestora.

Programare- procesul de creație programe de calculator. Într-un sens mai larg: gama de activități asociate cu crearea și întreținerea muncii. starea programelor – software de calculator.

Tehnologia de programare- un set de metode și instrumente utilizate în procesul de dezvoltare software.

Tehnologia de programare este un set de instrucțiuni tehnologice, inclusiv:

· indicarea succesiunii operaţiilor tehnologice;

· enumerarea condițiilor în care se realizează cutare sau cutare operațiune;

· descrieri ale operațiunilor în sine, unde pentru fiecare operație sunt definite datele inițiale, rezultatele, precum și instrucțiunile, regulamentele, standardele, criteriile etc.

Tehnologie moderna programare - abordarea componentelor, care implică construirea de software din componente individuale - bucăți de software separate fizic care interacționează între ele prin interfețe binare standardizate. În prezent criterii de calitate un produs software este considerat a fi: − funcţionalitate; − fiabilitate;− ușurință în utilizare;− eficienţă(raportul dintre nivelul serviciilor furnizate de un produs software unui utilizator în condiții date și volumul resurselor utilizate); mentenabilitatea(caracteristicile unui produs software care permit reducerea la minimum a eforturilor de a face modificări pentru a elimina erorile din acesta și pentru a-l modifica în conformitate cu nevoile în schimbare ale utilizatorilor); mobilitate(capacitatea unui sistem software de a fi transferat dintr-un mediu în altul, în special, de la un computer la altul).

O etapă importantă în crearea unui produs software este testare și depanare.

Depanare− aceasta este o activitate care vizează detectarea și corectarea erorilor dintr-un produs software folosind procesele de executare a programelor acestuia.

Testare− este procesul de executare a programelor sale pe un anumit set de date, pentru care se cunoaște dinainte rezultatul aplicării sau se cunosc regulile de comportare ale acestor programe.

Există următoarele metode de testare PS:

1) Testarea statică - testarea manuală a programului la masă.

2) Testare deterministă – cu diverse combinații de date sursă.

3) Stochastic – ref. Datele sunt selectate aleatoriu, iar rezultatul este determinat de calitatea rezultatelor sau de o estimare aproximativă.


Stiluri de programare.

Un stil de programare este un set de tehnici sau metode de programare pe care programatorii le folosesc pentru a produce programe care sunt corecte, eficiente, ușor de utilizat și ușor de citit.

Există mai multe stiluri de programare:

  1. Programare procedurală este programarea în care un program este o succesiune de instrucțiuni. Folosit în limbi de nivel înalt Basic, Fortran etc.
  2. Programare functionala este programarea în care un program este o secvență de apeluri de funcții. Folosit în Lisp și în alte limbi.
  3. Programare logica – aceasta este programarea în care programul este un set de determinare a relațiilor dintre obiecte. Folosit în Prolog și în alte limbi.

Programare orientată pe obiecte– aceasta este programarea în care baza programului este un obiect care este o colecție de date și reguli pentru transformarea lor. Folosit în Turbo-Pascal, C++ etc.

Calcule leneșe

ÎN limbi tradiționale programarea (de exemplu, C++) care apelează o funcție 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 apelului după valoare este apelul după necesitate. În acest caz, argumentul este evaluat numai dacă este necesar pentru a calcula rezultatul. Un exemplu al acestui comportament este operatorul de conjuncție din C++ (&&), care nu evaluează valoarea celui de-al doilea argument dacă primul argument este fals.

Dacă un limbaj funcțional nu acceptă evaluarea leneșă, atunci se numește strict. De fapt, în astfel de limbi ordinea evaluării este strict definită. Exemplele de limbaje stricte includ Scheme, Standard ML și Caml.

Limbile care folosesc evaluarea leneșă sunt numite non-strict. Haskell este un limbaj liber, la fel ca Gofer și Miranda, de exemplu. Limbile laxe sunt adesea pure.

Foarte des, limbile stricte includ mijloace pentru a sprijini unele caracteristici utile, inerente limbilor non-strictive, cum ar fi listele infinite. ÎN Livrare Standard ML conține un modul special pentru a suporta calcule amânate. În plus, Objective Caml acceptă cuvântul rezervat suplimentar lazy și un construct pentru liste de valori calculate după cum este necesar.

Această secțiune oferă scurta descriere unele limbaje de programare funcționale (foarte puține).

§ Lisp(Procesor de listă). Este considerat primul limbaj de programare funcțional. Nedactilografiat. Conține o mulțime de proprietăți imperative, dar în general încurajează stilul funcțional de programare. Utilizează apelul după valoare pentru calcule. Există un dialect al limbii orientat pe obiecte - CLOS.

§ EU ÎNOT(Dacă vezi ce vreau să spun). Prototip de limbaj funcțional. Dezvoltat de Landin în anii 60 ai secolului XX pentru a demonstra ce ar putea fi un limbaj de programare funcțional. Alături de limbaj, Landin a dezvoltat și o mașină virtuală specială pentru executarea programelor pe ISWIM. Această mașină virtuală de apel după valoare se numește o mașină SECD. Sintaxa multor limbaje funcționale se bazează pe sintaxa limbajului ISWIM. Sintaxa lui ISWIM este similară cu cea a ML, în special Caml.

§ Sistem. Un dialect Lisp destinat cercetării științifice în domeniul informaticii. Schema a fost concepută cu accent pe eleganța și simplitatea limbajului. Acest lucru face ca limbajul să fie mult mai mic decât Common Lisp.


§ M.L.(Meta limbaj). O familie de limbaje stricte cu un sistem de tip polimorf dezvoltat și module parametrizabile. ML este predat în multe universități occidentale (unele chiar ca primul limbaj de programare).

§ ML standard. Unul dintre primele limbaje de programare funcționale tipizate. Conține unele proprietăți imperative, cum ar fi link-uri către valori modificabileși prin urmare nu este pur. Utilizează apelul după valoare pentru calcule. O implementare foarte interesantă a modularității. Sistem puternic de tip polimorf. Cel mai recent standard de limbaj este Standardul ML-97, pentru care există definiții matematice formale ale sintaxei, precum și semantica statică și dinamică a limbajului.

§ Caml LightȘi Obiectiv Caml. Ca și Standard ML, aparține familiei ML. Objective Caml diferă de Caml Light în principal prin suportul său pentru programarea clasică orientată pe obiecte. La fel ca Standard ML este strict, dar are suport încorporat pentru evaluarea leneșă.

§ Miranda. Dezvoltat de David Turner ca un limbaj funcțional standard care a folosit evaluarea leneșă. Are un sistem strict de tip polimorf. La fel ca ML, este predat în multe universități. Prevăzut influență mare pentru dezvoltatorii de limbaj Haskell.

§ Haskell. Una dintre cele mai comune limbi non-strictive. Are un sistem de tastare foarte dezvoltat. Sistemul de module este oarecum mai puțin bine dezvoltat. Cel mai recent standard de limbă este Haskell-98.

§ Gofer(Bine pentru raționamentul ecuațional). Dialect Haskell simplificat. Proiectat pentru predarea programării funcționale.

§ Curat. Proiectat special pentru programare paralelă și distribuită. Sintaxa este similară cu Haskell. Curat. Utilizează calcule amânate. Compilatorul vine cu un set de biblioteci (biblioteci I/O) care vă permit să programați grafic interfața cu utilizatorul sub Win32 sau MacOS.

Reamintim că cea mai importantă caracteristică a abordării funcționale este faptul că orice program dezvoltat într-un limbaj de programare funcțional poate fi considerat o funcție, ale cărei argumente pot fi și funcții.

Abordarea funcțională a dat naștere unei întregi familii de limbi, al căror strămoș, după cum sa menționat deja, a fost limbajul de programare LISP. Mai târziu, în anii 70, a fost dezvoltată versiunea originală a limbajului ML, care s-a dezvoltat ulterior, în special, în SML, precum și într-o serie de alte limbi. Dintre acestea, poate „cea mai tânără” este limba Haskell, creată destul de recent, în anii 90.

Un avantaj important al implementării limbajelor de programare funcționale este alocarea dinamică automată a memoriei computerului pentru stocarea datelor. În acest caz, programatorul scapă de necesitatea de a controla datele și, dacă este necesar, poate rula funcția „colectare gunoi” - ștergerea memoriei de datele de care programul nu va mai avea nevoie.

Programe complexe cu abordare funcțională sunt construite prin agregarea funcţiilor. În acest caz, textul programului este o funcție, unele dintre argumentele căreia pot fi considerate și funcții. Astfel, reutilizarea codului se reduce la apelarea unei funcții descrise anterior, a cărei structură, spre deosebire de o procedură de limbaj imperativ, este transparentă din punct de vedere matematic.

Deoarece o funcție este un formalism natural pentru limbajele de programare funcționale, implementarea diferitelor aspecte ale programării legate de funcții este mult simplificată. Scrisul devine intuitiv transparent funcții recursive, adică funcții care se numesc argumente. Implementarea prelucrării structurilor recursive de date devine, de asemenea, firească.

Datorită implementării unui mecanism de potrivire a modelelor, limbaje funcționale de programare, cum ar fi ML și Haskell, sunt bune pentru procesarea simbolică.

Desigur, limbajele de programare funcționale nu sunt lipsite de unele dezavantaje.

Acestea includ adesea structura neliniară a programului și eficiența relativ scăzută a implementării. Cu toate acestea, primul dezavantaj este destul de subiectiv, iar al doilea a fost depășit cu succes de implementări moderne, în special, o serie de traducători de limbaj SML recent, inclusiv un compilator pentru mediul Microsoft .NET.

Pentru a dezvolta software profesional în limbaje de programare funcționale, trebuie să înțelegeți profund natura funcției.

Rețineți că sub termenul „funcție” în formalizarea matematică și implementare software se referă la concepte diferite.

Astfel, o funcție matematică f cu un domeniu de definiție A și un interval de valori B este mulțimea de perechi ordonate

astfel încât dacă

(a,b 1) f și (a,b 2) f,

La rândul său, o funcție într-un limbaj de programare este un construct al acestui limbaj care descrie regulile de conversie a unui argument (așa-numitul parametru real) într-un rezultat.

Pentru a oficializa conceptul de „funcție”, a fost construită o teorie matematică cunoscută sub numele de calcul lambda. Mai precis, acest calcul ar trebui numit calculul conversiilor lambda.

Conversia se referă la transformarea obiectelor de calcul (și în programare, funcții și date) dintr-o formă în alta. Sarcina inițialăîn matematică a existat dorinţa de a simplifica forma expresiilor. În programare, această sarcină specială nu este atât de semnificativă, deși, așa cum vom vedea mai târziu, utilizarea calculului lambda ca formalizare inițială poate ajuta la simplificarea tipului de program, de exemplu. conduce la optimizarea codului programului.

În plus, conversiile oferă o tranziție către notațiile nou introduse și, astfel, permit reprezentarea domeniului subiectului într-o formă mai compactă sau mai detaliată sau, în limbajul matematic, schimbă nivelul de abstractizare în raport cu domeniul subiectului. Această caracteristică este, de asemenea, utilizată pe scară largă de limbajele de programare orientate pe obiecte și modulare structurate în ierarhia obiectelor, fragmentelor de program și structurilor de date. Interacțiunea componentelor aplicației în .NET se bazează pe același principiu. În acest sens, trecerea la noile notații este unul dintre cele mai importante elemente ale programării în general, iar calculul lambda (spre deosebire de multe alte ramuri ale matematicii) reprezintă o modalitate adecvată de formalizare a renotațiilor.

Să sistematizăm evoluția teoriilor care stau la baza abordării moderne a calculului lambda.

Să luăm în considerare evoluția limbajelor de programare care se dezvoltă în cadrul abordării funcționale.

Limbajele de programare funcționale timpurii, care provin din limbajul clasic LISP (LISt Processing), au fost concepute pentru a procesa liste, de exemplu. informație simbolică. În acest caz, principalele tipuri au fost un element atomic și o listă de elemente atomice, iar accentul principal a fost pe analizarea conținutului listei.

Dezvoltarea limbajelor de programare timpurii a devenit limbaje de programare funcționale cu tastare puternică, un exemplu tipic aici este clasicul ML și descendentul său direct SML. În limbajele puternic tipizate, fiecare construct (sau expresie) trebuie să aibă un tip.

Cu toate acestea, în limbajele de programare funcționale ulterioare nu este nevoie de atribuirea explicită a tipului, iar tipurile de expresii inițial nedefinite, ca în SML, pot fi deduse (înainte de rularea programului) pe baza tipurilor de expresii asociate acestora. .

Următorul pas în dezvoltarea limbajelor de programare funcționale a fost suportul funcțiilor polimorfe, adică. funcții cu argumente parametrice (analogii unei funcții matematice cu parametri). În special, polimorfismul este acceptat în SML, Miranda și Haskell.

În stadiul actual de dezvoltare, limbaje de programare funcționale de „nouă generație” au apărut cu următoarele capacități avansate: potrivirea modelelor (Scheme, SML, Miranda, Haskell), polimorfismul parametric (SML) și așa-numitul „lenes” (ca necesare) calcule (Haskell, Miranda, S.M.L.).

Familia de limbaje funcționale de programare este destul de mare. Acest lucru este dovedit nu atât de lista semnificativă de limbi, cât de faptul că multe limbi au dat naștere unor tendințe întregi în programare. Să ne amintim că LISP a dat naștere unei întregi familii de limbi: Scheme, InterLisp, COMMON Lisp etc.

Limbajul de programare SML nu a făcut excepție, care a fost creat sub forma limbajului ML de R. Milner la MIT (Massachusetts Institute of Technology) și a fost inițial destinat deducțiilor logice, în special, dovedirii teoremelor. Limbajul este strict tipificat și lipsit de polimorfism parametric.

Dezvoltarea ML „clasică” a devenit trei limbi moderne cu capacități aproape identice (polimorfism parametric, potrivire a modelelor, calcule „leneșe”). Acesta este limbajul SML, dezvoltat în Marea Britanie și SUA, CaML, creat de un grup de oameni de știință francezi de la Institutul INRIA, SML/NJ - un dialect al SML din New Jersey și, de asemenea, o dezvoltare rusă - mosml („ dialectul Moscovei" din ML).

Apropierea de formalizarea matematică și orientarea funcțională inițială a determinat următoarele avantaje ale abordării funcționale:

1. ușurința de testare și verificare a codului programului pe baza posibilității de a construi o demonstrație matematică riguroasă a corectitudinii programelor;

2. unificarea prezentării programului și datelor (datele pot fi încapsulate în program ca argumente de funcție, desemnarea sau calculul valorii funcției se poate face după caz);

3. tastare sigură: operațiunile de date nevalide sunt excluse;

4. tastare dinamică: este posibilă detectarea erorilor de tastare în timpul execuției (absența acestei proprietăți în limbajele de programare funcționale timpurii poate duce la umplerea memoriei RAM a computerului);

5. independenţa implementării software-ului faţă de reprezentarea datelor maşinii şi Arhitectura sistemului programe (programatorul se concentrează pe detaliile implementării și nu pe caracteristicile reprezentării datelor mașinii).

Rețineți că realizarea beneficiilor pe care limbajele de programare funcționale le oferă depinde în mod semnificativ de alegerea platformei software și hardware.

Dacă este selectat ca platforma software Tehnologia .NET, aproape indiferent de implementarea hardware, programatorul sau managerul de proiect software primește suplimentar următoarele beneficii:

1. integrarea diferitelor limbaje de programare funcționale (în timp ce maximizează avantajele fiecărei limbi, în special, Scheme oferă un mecanism de potrivire a modelelor, iar SML oferă capacitatea de a calcula după cum este necesar);

2. integrarea diferitelor abordări ale programării bazate pe Infrastructura Limbajului Comun, sau CLI (în special, este posibil să se utilizeze C# pentru a oferi avantajele unei abordări orientate pe obiecte și SML - funcțional, ca în acest curs) ;

3. sistem de tastare unificat comun Common Type System, CTS (gestionarea uniformă și sigură a tipurilor de date în program);

4. sistem flexibil, în mai multe etape, pentru asigurarea securității codului programului (în special, bazat pe mecanismul de asamblare).

Principalele caracteristici ale limbajelor de programare funcționale care le deosebesc atât de limbajele imperative, cât și de limbajele de programare logică sunt transparența referențială și determinismul. În limbajele funcționale, există o variație semnificativă a parametrilor, cum ar fi regulile de tastare și de calcul. În multe limbi, ordinea evaluării este strict definită. Dar uneori limbile stricte conțin suport pentru unele elemente utile inerente limbilor non-strictive, cum ar fi liste infinite (Standard ML are un modul special pentru a sprijini evaluarea leneșă). În schimb, limbajele non-strictive permit calculul energetic în unele cazuri.

Astfel, Miranda are semantică leneșă, dar vă permite să specificați constructori stricti prin marcarea argumentelor constructorului într-un anumit mod.

Mulți limbile moderne limbajele de programare funcționale sunt limbaje puternic tipizate (tactilare puternică). Tastarea puternică oferă o securitate mai mare. Multe erori pot fi corectate în etapa de compilare, astfel încât etapa de depanare și timpul general de dezvoltare a programului sunt reduse. Tastarea puternică permite compilatorului să genereze cod mai eficient și astfel să accelereze execuția programului. Alături de aceasta, există limbaje funcționale cu tastare dinamică. Tipul de date în astfel de limbi este determinat în timpul execuției programului (Capitolul 3). Ele sunt uneori numite „fără tip”. Avantajele lor includ faptul că programele scrise în aceste limbi au o generalitate mai mare. Un dezavantaj poate fi considerat atribuirea multor erori fazei de execuție a programului și necesitatea asociată de a utiliza funcții de verificare a tipului și o reducere corespunzătoare a generalității programului. Limbile tastate contribuie la generarea unui cod mai „de încredere”, în timp ce limbile tastate produc un cod mai „general”.

Următorul criteriu după care limbajele de programare funcționale pot fi clasificate poate fi prezența mecanismelor imperative. În același timp, se obișnuiește să se numească limbaje de programare funcționale care sunt lipsite de mecanisme imperative „pure”, iar cele care le au sunt numite „impure”. În prezentarea de mai jos a limbajelor de programare funcționale, limbajele de programare vor fi denumite „practice” și „academice”. Limbajele „practice” sunt înțelese ca limbaje care au o aplicație comercială (în ele s-au dezvoltat aplicații reale sau au existat sisteme de programare comerciale). Limbajele de programare academice sunt populare în cercurile de cercetare și în educația informatică, dar practic nu există aplicații comerciale scrise în astfel de limbaje. Ele rămân doar un instrument pentru efectuarea cercetărilor teoretice în domeniul informaticii și sunt utilizate pe scară largă în procesul educațional.

O listă a celor mai populare limbaje de programare funcțională este prezentată mai jos folosind următoarele criterii: informații generale; tastare; tip de calcul; puritate.

Lisp comun. O versiune de Lisp care poate fi considerată un standard lingvistic încă din 1970, datorită sprijinului din partea Laboratorului de Inteligență Artificială a Institutului de Tehnologie din Massachusetts, fără tip, energică, cu set mare incluziuni imperative care permit atribuirea și distrugerea structurilor. Practic. Este suficient să spunem că editorul de grafică vectorială AutoCAD a fost scris în Lisp.

Sistem. Un dialect Lisp destinat cercetării științifice în informatică și predării programării funcționale. Datorită lipsei incluziunilor imperative, limbajul este mult mai mic decât Common Lisp. Datând dintr-un limbaj dezvoltat de J. McCarthy în 1962. Academic, fără tip, energic, curat.

Refal. O familie de limbi dezvoltate de V. F. Turchin. Cel mai în vârstă membru al acestei familii a fost realizat pentru prima dată în 1968 în Rusia. Este încă folosit pe scară largă în cercurile academice. Conține elemente de programare logică (potrivire de model). Prin urmare, limbajul Refal este propus în aceasta manual ca limbaj pentru auto-studiu.

Miranda. Tastat puternic, acceptă tipuri de date utilizator și polimorfism. Dezvoltat de Turner pe baza limbajelor anterioare SALS și KRC. Are semantică leneșă. Fără incluziuni imperative.

Haskell. Dezvoltarea limbii a avut loc la sfârșitul secolului trecut. Cunoscut pe scară largă în cercurile academice. În unele universități occidentale este folosită ca limbă principală pentru studenți. Unul dintre cele mai puternice limbaje funcționale. Limbajul leneș. Limbajul pur funcțional. Dactilografiat. Haskell este un instrument excelent pentru învățare și experimentare cu tipuri de date funcționale complexe. Programele scrise în Haskell au o dimensiune semnificativă a codului obiect și o viteză scăzută de execuție.

Curat. Un dialect Haskell adaptat nevoilor de programare practică. Ca și Haskell, este un limbaj leneș, pur funcțional, care conține clase de tip. Dar Clean conține și caracteristici interesante care nu au echivalent în Haskell. De exemplu, caracteristicile imperative din Clean se bazează pe tipuri unice, ideea cărora este împrumutată din logica liniară. Clean conține mecanisme care pot îmbunătăți semnificativ eficiența programelor. Aceste mecanisme suprimă în mod explicit calculul leneș. Implementarea Clean este un produs comercial, dar o versiune gratuită este disponibilă pentru cercetare și în scopuri educaționale.

ML (Meta Language). Dezvoltat de un grup de programatori condus de Robert Milier la mijlocul anilor '70. în Edinburgh (Edinburgh Logic for Computable Functions). Ideea limbajului a fost de a crea un mecanism pentru construirea de dovezi formale într-un sistem de logică pentru funcții calculabile. În 1983, limbajul a fost revizuit pentru a include concepte precum modulele. A ajuns să fie numit ML standard. ML este un limbaj puternic tipizat, cu verificare statică a tipului și execuție de program aplicativ. A câștigat o mare popularitate în cercurile de cercetare și în domeniul educației informatice.

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 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 non-bug din cauza faptului că funcționarea funcției depinde de starea externă și de 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 limbajele de programare funcționale sunt atât de rar utilizate în aplicațiile multithreaded? 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 de scalat ca sisteme tipice, dezvoltat 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 automata funcții și găsirea candidaților potriviți pentru paralelizare este o sarcină trivială, cum ar fi inline automată! Î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ă noul tău procesor va arăta doar câștiguri în programele concepute având în vedere paralelizarea. Există foarte puține dintre acestea printre software-ul imperativ. Dar 100% dintre programele funcționale sunt 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). Sistemele Unix au avut un model de actualizare 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, încă nu este acceptabilă pentru o clasă mare de aplicații server. 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, toate operațiile matematice utilizate pe hârtie pot fi aplicate programelor 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. Bazele de date relaționale fac astfel de optimizări de ani de zile. Nimic nu vă împiedică să utilizați tehnici similare în programele obișnuite.

Î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 esențiale. Dacă evoluțiile dumneavoastră nu sunt în zona critică aplicatii importante, atunci instrumentele automate de verificare vă vor 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 este aproape același cu conceptul de clase Java care operează una pe cealaltă (putem trece clase altor 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 anumită secțiune a codului se repetă în locuri diferite, îl puneți într-o funcție separată (din fericire, școlile încă învață cum să facă asta). 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ă în diverse moduri și îl transmite către 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, în funcție de aceasta, să setăm codul corect client. De exemplu astfel:

Clasa MessageHandler ( void handleMessage(Message msg) ( // ... if(msg.getDestination().equals("server1") ( msg.setClientCode("ABCD_123"); ) else ( msg.setClientCode("123_ABC") ) // ... sendMessage(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. Obiect abordare orientată 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 există mult text pentru o schimbare atât de mică. 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, această tehnică simplă 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ă, iar compilatorul scrie codul necesar pentru tine! 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ă când 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 în același mod în care un matematician privește expresiile algebrice - poate anula lucruri, poate anula execuția anumitor secțiuni de cod, poate schimba ordinea apelurilor pentru o eficiență mai mare, chiar aranja codul în așa fel încât să reducă numărul de erori, asigurând totodată integritatea programului. Exact asta mare avantaj atunci când descrieți un program folosind primitive formale stricte - codul se supune legilor 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()) (trimite laSEC(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ă decât dacă(condiție booleană, cod Listă) (codul dacă(!condiție); )
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 de la un numar mare problemele și priviți-le de la un nivel superior (de exemplu, puteți utiliza funcții pentru procesarea listelor pe secvențe infinite).

Defecte

Desigur, brânza gratuită vine 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 ține cont de efectele lor secundare) și nu pot interacționa cu lumea exterioară! 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ă vă gândiți la fiecare funcție ca caz special continuări. Această conversie se poate face automat (de fapt, mulți compilatori fac acest lucru).

De îndată ce convertim programul în forma CPS, devine clar că fiecare instrucțiune are o continuare, o funcție căreia i se va trece rezultatul, care în program regulat ar fi un punct de provocare. 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 adăugarea 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 asta pentru programele 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ă” în 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 tradiționali care se mută pe Web, importanța continuărilor va crește doar în timp.

Potrivire de model

Potrivirea modelelor nu este atât de nouă sau idee 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. S-a remarcat că un numar mare de funcțiile conțin constructe 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 de potrivire a 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. Vine în minte bun exemplu cu 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. Rețineți că, în acest caz, variabilele nu sunt finale și funcția 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 un anumit grad. 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 numărător are propria sa copie a lui n, în ciuda faptului că ar fi trebuit să dispară după rularea funcției makeIncrementer. 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ă. Prin urmare, închiderea se face ca funcția normală despre care am discutat mai devreme, cu excepția faptului că are link suplimentar la variabilele înconjurătoare:

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, intenționez să scriu despre teoria categoriilor, monade, structuri funcționale de date, sisteme de tip în limbaje funcționale, multithreading funcțional, baze de date funcționale și multe altele. 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. Mulțimea X se numește domeniul de definire al funcției P, iar mulțimea Y ​​este domeniul valorilor funcției P. Valoarea x în P(x), care reprezintă orice element din mulțimea X, se numește variabilă independentă, iar valoarea y din mulțimea Y, definită de ecuația y = P(x ), se numește variabilă dependentă. Uneori, dacă funcția f nu este definită pentru toți x din X, se vorbește despre o funcție parțial definită, altfel înseamnă definiție completă.

Foarte proprietate importantă Definiția matematică a unei funcții este că, având în vedere un argument x, aceasta determină întotdeauna aceeași valoare deoarece nu are efecte secundare. Efectele secundare în limbajele de programare sunt asociate cu variabile care modelează celulele de memorie. Funcția matematică definește o valoare, mai degrabă decât specifică o secvență de operații asupra numerelor stocate în celulele de memorie pentru a calcula o anumită valoare. Nu există variabile în sensul dat în limbajele de programare imperative, deci nu pot exista efecte secundare.

Într-un limbaj de programare bazat pe conceptul matematic de funcție, pot fi reprezentate programe, proceduri și funcții. În cazul unui program, x corespunde datelor de intrare, iar y corespunde rezultatelor. În cazul unei proceduri sau funcție, x caracterizează parametrii, iar y caracterizează valorile returnate. În ambele cazuri, x poate fi denumit „intrări” și y ca „ieșiri”. Prin urmare, în abordarea funcțională a programării, nu există nicio distincție între program, procedură și funcție. Pe de altă parte, cantitățile de intrare și de ieșire sunt întotdeauna diferite.

Limbajele de programare au o separare foarte clară între definirea unei funcții și utilizarea unei funcții. O definiție a funcției descrie modul în care o cantitate poate fi calculată pe baza parametrilor formali. O aplicație de funcție este să apeleze o anumită funcție folosind parametri reali. Rețineți că în matematică diferența dintre un parametru și o variabilă nu este întotdeauna evidentă. Foarte des termenul „parametru” este înlocuit cu termenul „variabilă independentă”. De exemplu, la matematică puteți scrie definiția funcției de pătrat: pătrat(x) = x * x

Să presupunem că x satisface pătrat(x) + 2 . . .

Principala diferență dintre programarea imperativă și cea funcțională este interpretarea conceptului de variabilă. În matematică, variabilele sunt reprezentate ca valori reale, iar în limbajele de programare imperative, variabilele se referă la locațiile de memorie în care sunt stocate valorile lor. Atribuțiile vă permit să schimbați valorile în aceste zone de memorie. În schimb, în ​​matematică nu există concepte de „zonă de stocare” și „atribuire”, deci un operator precum x = x + 1

nu are sens. Programarea funcțională se bazează pe o abordare matematică a conceptului de „variabilă”. În programarea funcțională, variabilele sunt asociate cu valori, dar nu au nimic de-a face cu locațiile de memorie. După legarea unei variabile la o valoare, valoarea variabilei se modifică

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 facilitează proiectarea și suportul ulterior al sistemelor software mari. 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 sens, 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;

    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/ - Revizuirea 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 Semantică: funcțional Tip de execuție: compilare la bytecode pentru o 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 Semantics: funcțional, procedural, declarativ, orientat pe obiecte, 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 multi-paradigme conceput de Timothy Budd. Limba Leda a fost creată inițial pentru a se combina programare imperativă, obiectiv...... 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, blocul principal este concept matematic funcții. 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