Si të konfiguroni telefonat inteligjentë dhe PC. Portali informativ
  • në shtëpi
  • Siguria
  • Programim funksional për të gjithë. Elementet e programimit funksional

Programim funksional për të gjithë. Elementet e programimit funksional

Vargu i kundërt (arg i vargut) (nëse (arg.gjatësia == 0) (arg kthimi;) else (kthimi i kundërt (arg.nënstring (1, arg.gjatësia)) + arg.nënstring (0, 1);))
Ky funksion është mjaft i ngadalshëm sepse ri-thirre veten. Një rrjedhje e kujtesës është e mundur këtu, pasi objektet e përkohshme krijohen shumë herë. Por ky është një stil funksional. Mund t'ju tregohet e çuditshme se si njerëzit mund të programojnë në atë mënyrë. Epo, sapo isha gati t'ju tregoja.

Përfitimet e programimit funksional

Ju ndoshta mendoni se nuk mund të argumentoj për të justifikuar funksionin monstruoz të mësipërm. Kur fillova të mësoja programimin funksional, mendova gjithashtu. Isha gabim. Ka argumente shumë të mira për këtë stil. Disa prej tyre janë subjektive. Për shembull, programuesit pretendojnë se programet funksionale janë më të lehta për t'u kuptuar. Nuk do të jap argumente të tilla, sepse të gjithë e dinë që lehtësia e të kuptuarit është një gjë shumë subjektive. Për fatin tim, ka ende shumë argumente objektive.

Testimi i njësisë

Meqenëse çdo simbol në FP është i pandryshueshëm, funksionet nuk kanë efekte anësore. Ju nuk mund të ndryshoni vlerat e variablave, për më tepër, një funksion nuk mund të ndryshojë një vlerë jashtë fushëveprimit të tij, dhe në këtë mënyrë të ndikojë në funksione të tjera (siç mund të ndodhë me fushat e klasës ose variablat globale). Kjo do të thotë që rezultati i vetëm i një funksioni është vlera e kthimit. Dhe e vetmja gjë që mund të ndikojë në vlerën e kthimit janë argumentet e kaluara në funksion.

Këtu është, ëndrra blu e testuesve të njësive. Ju mund të testoni çdo funksion në programin tuaj duke përdorur vetëm argumentet që dëshironi. Nuk ka nevojë të thirrni funksionet në rendin e duhur ose të rikrijoni gjendjen e duhur të jashtme. E tëra çfarë ju duhet të bëni është të kaloni argumente që përputhen me rastet e skajeve. Nëse të gjitha funksionet në programin tuaj kalojnë testet e njësisë, atëherë mund të jeni shumë më të sigurt në cilësinë e softuerit tuaj sesa në rastin e gjuhëve të programimit imperativ. Në Java ose C ++, kontrollimi i vlerës së kthimit nuk është i mjaftueshëm - funksioni mund të ndryshojë gjendjen e jashtme, e cila gjithashtu duhet të kontrollohet. Nuk ka një problem të tillë në FP.

Korrigjimi

Nëse një program funksional nuk sillet siç prisni, atëherë korrigjimi i gabimeve është një fllad. Ju gjithmonë mund ta riprodhoni problemin, sepse gabimi në funksion nuk varet nga kodi i jashtëm që është ekzekutuar më parë. Në një program imperativ, gabimi shfaqet vetëm për një kohë. Do t'ju duhet të kaloni nëpër një sërë hapash që nuk lidhen me gabimet, sepse një funksion varet nga gjendja e jashtme dhe efektet anësore të funksioneve të tjera. Në FP, situata është shumë më e thjeshtë - nëse vlera e kthimit është e gabuar, atëherë do të jetë gjithmonë e gabuar, pavarësisht se cilat pjesë të kodit janë ekzekutuar më parë.

Pasi të riprodhoni gabimin, gjetja e burimit është e parëndësishme. Madje është bukur. Sapo të ndaloni ekzekutimin e programit, do të keni para vetes të gjithë grupin e thirrjeve. Ju mund të shikoni argumentet e thirrjes së secilit funksion, ashtu si në një gjuhë imperative. Me ndryshimin se kjo nuk mjafton në një program imperativ, sepse funksionet varen nga vlerat e fushave, variablave globale dhe gjendjeve të klasave të tjera. Një funksion në FP varet vetëm nga argumentet e tij dhe ky informacion është pikërisht para syve tuaj! Për më tepër, në një program imperativ, kontrollimi i vlerës së kthyer nuk është i mjaftueshëm për të treguar nëse një pjesë e kodit po sillet siç duhet. Do t'ju duhet të gjuani dhjetëra objekte jashtë funksionit për t'u siguruar që gjithçka po funksionon siç duhet. Në programimin funksional, gjithçka që duhet të bëni është të shikoni vlerën e kthimit!

Ndërsa ecni nëpër pirg, vini re që argumentet kalohen dhe vlerat e kthimit. Sapo vlera e kthimit të devijojë nga norma, ju futeni më thellë në funksion dhe vazhdoni përpara. Kjo përsëritet disa herë derisa të gjeni burimin e gabimit!

Multithreading

Programi funksional është menjëherë gati për paralelizim pa asnjë ndryshim. Nuk duhet të shqetësoheni për ngërçet apo kushtet e garës sepse nuk keni nevojë për bravë! Asnjë pjesë e vetme e të dhënave në një program funksional nuk ndryshohet dy herë nga i njëjti rrymë ose nga të ndryshëm. Kjo do të thotë që ju mund të shtoni lehtësisht tema në programin tuaj pa menduar as për problemet e natyrshme në gjuhët imperative.

Nëse është kështu, atëherë pse gjuhët funksionale të programimit përdoren kaq rrallë në aplikacione me shumë fije? Më shpesh sesa mendoni, në fakt. Ericsson ka zhvilluar një gjuhë funksionale të quajtur Erlang për përdorim në çelsat e telekomunikacionit tolerantë ndaj gabimeve dhe të shkallëzuara. Shumë njerëz vunë re avantazhet e Erlang dhe filluan ta përdorin atë. Ne po flasim për sistemet e telekomunikacionit dhe kontrollit të trafikut që nuk përshkallëzohen aq lehtë sa sistemet tipike të zhvilluara në Wall Street. Në përgjithësi, sistemet e shkruara në Erlang nuk janë aq të shkallëzueshme dhe të besueshme sa sistemet Java. Sistemet Erlang janë super të besueshme.

Historia e multithreading nuk mbaron këtu. Nëse jeni duke shkruar një aplikacion në thelb me një fije të vetme, përpiluesi mund të optimizojë një program funksional për të përdorur shumë CPU. Le të hedhim një vështrim në pjesën tjetër të kodit.


Një përpilues gjuhësor funksional mund të analizojë kodin, të klasifikojë funksionet që krijojnë vargjet s1 dhe s2 si funksione që konsumojnë kohë dhe t'i ekzekutojë ato paralelisht. Kjo nuk mund të bëhet në një gjuhë imperative, sepse çdo funksion mund të ndryshojë gjendjen e jashtme dhe kodi menjëherë pas thirrjes mund të varet nga ai. Në FP, analiza automatike e funksioneve dhe kërkimi i kandidatëve të përshtatshëm për paralelizim është po aq i parëndësishëm sa një inline automatik! Në këtë kuptim, stili funksional i programimit plotëson kërkesat e së ardhmes. Zhvilluesit e harduerit nuk mund ta bëjnë më CPU-në të funksionojë më shpejt. Në vend të kësaj, ata po rrisin numrin e bërthamave dhe po pretendojnë një rritje katërfish në shpejtësinë e llogaritjes me shumë fije. Sigurisht, ata harrojnë të thonë shumë mirë me kohë se procesori juaj i ri do të tregojë një rritje vetëm në programet e zhvilluara duke pasur parasysh paralelizmin. Ka shumë pak prej tyre në mesin e softuerëve imperativë. Por 100% e programeve funksionale janë gati për multithreading jashtë kutisë.

Shpërndarja e nxehtë

Në kohët e vjetra, duhej të rindizje kompjuterin për të instaluar përditësimet e Windows. Shume here. Pas instalimit të një versioni të ri të luajtësit të mediave. Ka pasur ndryshime të rëndësishme në Windows XP, por situata është ende larg idealit (sot kam ekzekutuar Windows Update në punë dhe tani kujtesa e bezdisshme nuk do të më lërë të qetë derisa të rindizem). Në sistemet Unix, modeli i përmirësimit ishte më i mirë. Për të instaluar përditësime, duhej të ndaloje disa komponentë, por jo të gjithë OS. Megjithëse situata duket më e mirë, ajo ende nuk është e pranueshme për një klasë të madhe aplikacionesh serverësh. Sistemet e telekomunikacionit duhet të ndizen 100% të rasteve, sepse nëse, për shkak të përditësimit, një person nuk mund të telefonojë një ambulancë, atëherë mund të humbasin jetë. Firmat me Wall Streets gjithashtu nuk duan të mbyllin serverët gjatë fundjavës për të instaluar përditësime.

Në mënyrë ideale, ju duhet të përditësoni të gjitha pjesët e nevojshme të kodit pa e ndalur sistemin në parim. Në botën imperative, kjo është e pamundur [trans. në Smalltalk është shumë e mundur]. Imagjinoni të shkarkoni një klasë Java në fluturim dhe të rifreskoni një version të ri. Nëse do ta bënim këtë, atëherë të gjitha instancat e klasës do të bëheshin jofunksionale, sepse gjendja që ata mbanin do të humbiste. Do të na duhej të shkruanim kod të ndërlikuar për kontrollin e versionit. Ju do të duhet të serializoni të gjitha instancat e krijuara të klasës, pastaj t'i shkatërroni ato, të krijoni shembuj të klasës së re, të përpiqeni të ngarkoni të dhënat e serializuara me shpresën se migrimi do të shkojë mirë dhe instancat e reja do të jenë të vlefshme. Dhe përveç kësaj, kodi i migrimit duhet të shkruhet manualisht çdo herë. Dhe kodi i migrimit duhet gjithashtu të ruajë lidhjet midis objekteve. Në teori, gjithçka është në rregull, por në praktikë nuk do të funksionojë kurrë.

Në një program funksional, të gjitha gjendjet ruhen në stek si argumente funksioni. Kjo e bën vendosjen e nxehtë shumë më të lehtë! Në thelb, gjithçka që duhet të bëni është të llogarisni ndryshimin midis kodit në serverin e prodhimit dhe versionit të ri dhe të instaloni ndryshimet në kod. Pjesa tjetër do të bëhet automatikisht nga mjetet gjuhësore! Nëse mendoni se ky është fantashkencë, atëherë mendoni dy herë. Inxhinierët e Erlang kanë përditësuar sistemet e tyre për vite me rradhë pa i ndaluar ato.

Llogaritja dhe optimizimi i provave (provat dhe optimizimet me ndihmën e makinës)

Një veçori tjetër interesante e gjuhëve të programimit funksional është se ato mund të studiohen matematikisht. Meqenëse një gjuhë funksionale është një zbatim i një sistemi formal, të gjitha veprimet matematikore të përdorura në letër mund të aplikohen në programet funksionale. Përpiluesi, për shembull, mund të konvertojë një pjesë të kodit në një pjesë ekuivalente, por më efikase, duke vërtetuar matematikisht ekuivalencën e tyre. Bazat e të dhënave relacionale i kanë bërë këto optimizime prej vitesh. Asgjë nuk ju pengon të përdorni teknika të ngjashme në programet e zakonshme.

Për më tepër, mund të përdorni mjete matematikore për të vërtetuar korrektësinë e seksioneve të programeve tuaja. Nëse dëshironi, mund të shkruani mjete që analizojnë kodin dhe krijojnë automatikisht teste Unit për rastet e skajeve! Ky funksionalitet është i paçmuar për sistemet e forta shkëmbore. Kur zhvillohen sisteme për monitorimin e stimuluesve kardiak ose menaxhimin e trafikut ajror, këto mjete janë thelbësore. Nëse zhvillimet tuaja nuk janë në fushën e aplikacioneve kritike, atëherë mjetet e verifikimit automatik do t'ju japin akoma një avantazh të madh ndaj konkurrentëve tuaj.

Funksionet e rendit më të lartë

Mos harroni, kur fola për përfitimet e FP, vura re se "çdo gjë duket bukur, por është e kotë nëse më duhet të shkruaj në një gjuhë të ngathët në të cilën gjithçka është përfundimtare". Ky ishte një iluzion. Përdorimi i fundit në të gjithë vendin duket i ngathët vetëm në gjuhë programimi imperative si Java. Gjuhët e programimit funksional operojnë me lloje të tjera abstraksionesh, të tilla që ju harroni se dikur keni dashur të ndryshoni variablat. Një mjet i tillë janë funksionet e rendit më të lartë.

Në FP, një funksion nuk është i njëjtë me një funksion në Java ose C. Është një superset - ato mund të jenë të njëjta me funksionet Java dhe madje edhe më shumë. Le të themi se kemi një funksion në C:

Int add (int i, int j) (kthim i + j;)
Në FP, ky nuk është i njëjtë me një funksion të rregullt C. Le të zgjerojmë përpiluesin tonë Java për të mbështetur këtë shënim. Përpiluesi duhet ta kthejë deklaratën e funksionit në kodin e mëposhtëm Java (mos harroni se fundi i nënkuptuar është kudo):

Klasa add_function_t (int add (int i, int j) (kthim i + j;)) add_function_t add = new add_function_t ();
Simboli i shtimit nuk është në të vërtetë një funksion. Është një klasë e vogël me një metodë. Tani mund të kalojmë add si argument për funksionet e tjera. Mund ta shkruajmë në një simbol tjetër. Ne mund të krijojmë shembuj të add_function_t në kohën e ekzekutimit dhe ato do të mblidhen mbeturina nëse nuk nevojiten më. Funksionet bëhen objekte bazë si numrat dhe vargjet. Funksionet që veprojnë në funksione (marrini ato si argumente) quhen funksione të rendit më të lartë. Mos lejoni që kjo t'ju trembë. Koncepti i funksioneve të rendit më të lartë është pothuajse i njëjtë me konceptin e klasave Java që veprojnë mbi njëra-tjetrën (mund t'i kalojmë klasat në klasa të tjera). Mund t'i quajmë "klasa të rendit të lartë", por askush nuk shqetësohet me këtë, sepse Java nuk ka një komunitet të rreptë akademik pas saj.

Si dhe kur duhet të përdorni funksione të rendit më të lartë? Më vjen mirë që pyete. Ju shkruani programin tuaj si një pjesë të madhe, monolit të kodit pa u shqetësuar për hierarkinë e klasës. Nëse shihni se një pjesë e kodit përsëritet në vende të ndryshme, ju e zhvendosni atë në një funksion të veçantë (për fat të mirë, shkollat ​​ende mësojnë se si ta bëni këtë). Nëse vëreni se një pjesë e logjikës në funksionin tuaj duhet të sillet ndryshe në disa situata, atëherë krijoni një funksion të rendit më të lartë. Të hutuar? Këtu është një shembull i botës reale nga puna ime.

Supozoni se kemi një pjesë të kodit Java që merr një mesazh, e transformon atë në mënyra të ndryshme dhe e dërgon në një server tjetër.

Void handleMessage (mesazhi i mesazhit) (// ... msg.setClientCode ("ABCD_123"); // ... dërgo Mesazh (msg);) // ...)
Tani imagjinoni që sistemi ka ndryshuar dhe tani ju duhet të shpërndani mesazhe midis dy serverëve në vend të njërit. Gjithçka mbetet e pandryshuar, përveç kodit të klientit - serveri i dytë dëshiron ta marrë këtë kod në një format tjetër. Si ta përballojmë këtë situatë? Ne mund të kontrollojmë se ku duhet të shkojë mesazhi dhe të vendosim kodin e saktë të klientit bazuar në atë. Për shembull si kjo:

Klasa Message Handler (void handleMessage (message msg) (// ... if (msg.getDestination (). Është i barabartë ("server1") (msg.setClientCode ("ABCD_123");) tjetër (msg.setClientCode ("123_ABC") ;) // ... dërgo Mesazh (msg);) // ...)
Por kjo qasje nuk ka shkallë të mirë. Ndërsa shtohen serverë të rinj, funksioni do të rritet në mënyrë lineare dhe bërja e ndryshimeve do të bëhet një makth. Qasja e orientuar nga objekti është që të nënklasohet superklasa e përgjithshme MessageHandler dhe të nënklasohet logjika e përkufizimit të kodit të klientit:

Klasa abstrakte MessageHandler (void handleMessage (message msg) (// ... msg.setClientCode (getClientCode ()); // ... sendMessage (msg);) string abstrakt getClientCode (); // ...) klasa MessageHandlerOne zgjeron MessageHandler (String getClientCode () (kthehet "ABCD_123";)) klasa MessageHandlerTwo zgjeron MessageHandler (String getClientCode () (kthim "123_ABCD";))
Tani, për çdo server, ne mund të krijojmë një shembull të klasës përkatëse. Shtimi i të rejave nga serveri bëhet më i përshtatshëm. Por për një ndryshim kaq të vogël, shumë tekst. Duhej të krijoheshin dy lloje të reja vetëm për të shtuar mbështetje për kode të ndryshme klienti! Tani le të bëjmë të njëjtën gjë në gjuhën tonë me mbështetjen për funksionet e rendit më të lartë:

Klasa MessageHandler (void handleMessage (message message, Function getClientCode) (// ... Message msg1 = msg.setClientCode (getClientCode ()); // ... sendMessage (msg1);) // ...) String getClientCodeOne ( ) (kthejeni "ABCD_123";) String getClientCodeTwo () (kthejeni "123_ABCD";) mbajtës i mesazheve = mbajtës i ri i mesazheve (); handler.handleMessage (someMsg, getClientCodeOne);
Ne nuk krijuam lloje të reja apo ndërlikuam hierarkinë e klasave. Sapo e kaluam funksionin si parametër. Ne kemi arritur të njëjtin efekt si në homologun e orientuar nga objekti, me vetëm disa avantazhe. Ne nuk e lidhim veten me asnjë hierarki klase: ne mund të kalojmë çdo funksion tjetër në kohën e ekzekutimit dhe t'i ndryshojmë ato në çdo kohë, duke ruajtur një nivel të lartë modulariteti me më pak kod. Në thelb, përpiluesi krijoi ngjitësin e orientuar nga objekti për ne! Në të njëjtën kohë, të gjitha avantazhet e tjera të FP ruhen. Sigurisht, abstraksionet e ofruara nga gjuhët funksionale nuk mbarojnë këtu. Funksionet e rendit më të lartë janë vetëm fillimi

Currying

Shumica e njerëzve që takoj kanë lexuar Modelet e Dizajnit të Gang of Four. Çdo programues që respekton veten do të thotë se libri nuk është i lidhur me ndonjë gjuhë programimi të veçantë dhe modelet janë të zbatueshme për zhvillimin e softuerit në përgjithësi. Kjo është një deklaratë fisnike. Por fatkeqësisht është larg së vërtetës.

Gjuhët funksionale janë tepër shprehëse. Në një gjuhë funksionale, nuk keni nevojë për modele dizajni sepse gjuha është aq e nivelit të lartë sa mund të filloni lehtësisht të programoni në koncepte që përjashtojnë të gjitha modelet e njohura të programimit. Një model i tillë është përshtatësi (si ndryshon nga Fasada? Duket sikur dikush duhej të vuloste më shumë faqe për të përmbushur kushtet e kontratës). Ky model është i panevojshëm nëse gjuha ka mbështetje për currying.

Modeli i përshtatësit aplikohet më shpesh në njësinë "standarde" të abstraksionit në një klasë Java. Në gjuhët funksionale, modeli zbatohet për funksionet. Modeli merr një ndërfaqe dhe e transformon atë në një ndërfaqe tjetër, sipas kërkesave të caktuara. Këtu është një shembull i një modeli përshtatës:

Int pow (int i, int j); katror int (int i) (pow kthimi (i, 2);)
Ky kod përshtat ndërfaqen e një funksioni që ngre një numër në një fuqi arbitrare me ndërfaqen e një funksioni që vendos në katror një numër. Në qarqet akademike, kjo teknikë më e thjeshtë quhet currying (sipas logjikës Haskell Curry, i cili kreu një sërë trukesh matematikore për t'i zyrtarizuar të gjitha). Meqenëse funksionet përdoren zakonisht si argumente në FP, currying përdoret shumë shpesh për të sjellë funksionet në një ndërfaqe që nevojitet në një vend ose në një tjetër. Meqenëse ndërfaqja e një funksioni është argumentet e tij, currying përdoret për të reduktuar numrin e argumenteve (si në shembullin e mësipërm).

Ky mjet është ndërtuar në gjuhë funksionale. Nuk është e nevojshme të krijoni manualisht një funksion që mbështjell origjinalin. Një gjuhë funksionale do të bëjë gjithçka për ju. Si zakonisht, le ta zgjerojmë gjuhën tonë duke i shtuar edhe currying.

Sheshi = int pow (int i, 2);
Me këtë linjë, ne krijojmë automatikisht një funksion katror me një argument. Funksioni i ri do të thërrasë pow, duke zëvendësuar 2 për argumentin e dytë. Nga një këndvështrim Java, do të duket kështu:

Klasa katrori_funksioni_t (int katror (int i) (kthimi pow (i, 2);)) katrori_funksioni_t katror = katrori_funksioni_t i ri ();
Siç mund ta shihni, ne sapo shkruam një mbështjellës mbi funksionin origjinal. Në FP, currying është vetëm një mënyrë e thjeshtë dhe e përshtatshme për të krijuar mbështjellës. Ju përqendroheni në detyrë, dhe përpiluesi shkruan kodin e nevojshëm për ju! Është shumë e thjeshtë dhe ndodh sa herë që dëshironi të përdorni modelin e përshtatësit (mbështjellësit).

Vlerësim dembel

Vlerësimi dembel (ose dembel) është një teknikë interesante që bëhet e mundur sapo të kuptoni filozofinë funksionale. Ne kemi parë tashmë pjesën e mëposhtme të kodit kur folëm për multithreading:

Vargu s1 = disi i gjatëOperation1 (); Vargu s2 = disiOperacion i gjatë2 (); Vargu s3 = bashkoj (s1, s2);
Në gjuhët imperative të programimit, rendi i vlerësimit nuk ngre asnjë pyetje. Meqenëse çdo funksion mund të ndikojë ose të varet nga një gjendje e jashtme, është e nevojshme të vëzhgoni një renditje të qartë të thirrjeve: së pari, disiOperationLongOperation1, pastaj disi LongOperation2, dhe bashkohen në fund. Por jo gjithçka është kaq e thjeshtë në gjuhët funksionale.

Siç e pamë më herët, disiLongOperation1 dhe SomewhatLongOperation2 mund të nisen në të njëjtën kohë, sepse funksionet janë të garantuara që të mos preken dhe të mos varen nga gjendja globale. Por çfarë nëse nuk duam t'i ekzekutojmë ato në të njëjtën kohë, a duhet t'i thërrasim ato në mënyrë sekuenciale? Përgjigja është jo. Këto llogaritje duhet të kryhen vetëm nëse një funksion tjetër varet nga s1 dhe s2. Ne as nuk kemi nevojë t'i ekzekutojmë ato për sa kohë që na duhen brenda bashkimit. Nëse në vend të bashkimit zëvendësojmë një funksion që, në varësi të kushtit, përdor një nga dy argumentet, atëherë argumenti i dytë nuk duhet as të llogaritet! Haskell është një shembull i një gjuhe kompjuterike dembele. Haskell nuk garanton asnjë renditje të thirrjeve (fare!), Sepse Haskell ekzekuton kodin sipas nevojës.

Vlerësimi dembel ka disa avantazhe si dhe disa disavantazhe. Në pjesën tjetër, ne do të diskutojmë meritat dhe unë do të shpjegoj se si të përballemi me disavantazhet.

Optimizimi

Vlerësimi dembel ofron një potencial të jashtëzakonshëm për optimizim. Një përpilues dembel e trajton kodin tamam si një matematikan që studion shprehjet algjebrike - ai mund të zhbëjë disa gjëra, të anulojë ekzekutimin e disa seksioneve të kodit, të ndryshojë rendin e thirrjeve për efikasitet më të madh, madje të rregullojë kodin në atë mënyrë që të zvogëlojë numri i gabimeve, duke garantuar integritetin e programit. Ky është avantazhi më i madh kur përshkruani një program me primitivë të rreptë formal - kodi u bindet ligjeve matematikore dhe mund të studiohet me metoda matematikore.

Abstragimi i strukturave të kontrollit

Vlerësimi dembel siguron një nivel kaq të lartë abstraksioni, saqë gjërat e mahnitshme bëhen të mundshme. Për shembull, imagjinoni zbatimin e strukturës së mëposhtme të kontrollit:

Përveç nëse (stock.isEuropean ()) (sendToSEC (stock);)
Ne duam që funksioni sendToSEC të ekzekutohet vetëm nëse fondi (stoku) nuk është evropian. Si mund të zbatoni nëse? Pa vlerësim dembel, do të na duhej një sistem makro, por në gjuhë si Haskell, kjo nuk është e nevojshme. Ne mund të deklarojmë përveç nëse si funksion!

I pavlefshëm përveç rastit (kushti boolean, kodi i listës) ​​(nëse (! Kushti) kodi;)
Vini re se kodi nuk do të ekzekutohet nëse kushti == i vërtetë. Në gjuhët e forta, kjo sjellje nuk mund të përsëritet, pasi argumentet do të vlerësohen më parë nëse nuk thirret.

Struktura të pafundme të të dhënave

Gjuhët dembelë ju lejojnë të krijoni struktura të pafundme të dhënash, të cilat janë shumë më të vështira për t'u krijuar në gjuhë strikte [trans. - jo në Python]. Për shembull, imagjinoni një sekuencë Fibonacci. Natyrisht, ne nuk mund të llogarisim një listë të pafundme në kohë të fundme dhe ta ruajmë atë në memorie. Në gjuhë të forta si Java, ne thjesht do të shkruanim një funksion që kthen një anëtar arbitrar të një sekuence. Në gjuhë si Haskell, ne mund të abstraktojmë dhe thjesht të deklarojmë një listë të pafundme numrash Fibonacci. Meqenëse gjuha është dembel, do të llogariten vetëm pjesët e nevojshme të listës që përdoren aktualisht në program. Kjo ju lejon të abstraktoni nga një numër i madh problemesh dhe t'i shikoni ato nga një nivel më i lartë (për shembull, mund të përdorni funksionet e përpunimit të listave në sekuenca të pafundme).

Të metat

Sigurisht, djathi falas është vetëm në një kurth miu. Vlerësimi dembel ka një sërë disavantazhesh. Këto janë kryesisht disavantazhe nga dembelizmi. Në realitet, shpesh nevojitet një renditje e drejtpërdrejtë e llogaritjes. Merrni, për shembull, kodin e mëposhtëm:


Në një gjuhë dembel, askush nuk garanton që rreshti i parë do të ekzekutohet para të dytës! Kjo do të thotë që ne nuk mund të bëjmë I / O, nuk mund të përdorim normalisht funksionet vendase (në fund të fundit, ato duhet të thirren në një rend të caktuar për të marrë parasysh efektet e tyre anësore), dhe ne nuk mund të ndërveprojmë me botën e jashtme! Nëse prezantojmë një mekanizëm për të thjeshtuar ekzekutimin e kodit, atëherë do të humbasim avantazhin e ashpërsisë matematikore të kodit (dhe më pas do të humbasim të gjitha të mirat e programimit funksional). Për fat të mirë, ende nuk ka humbur gjithçka. Matematikanët filluan të punojnë dhe dolën me disa truke për t'u siguruar që udhëzimet ishin në rendin e duhur pa humbur frymën e tyre funksionale. Ne morëm më të mirën e të dy botëve! Teknika të tilla përfshijnë vazhdimin, monadat dhe shtypjen e veçantisë. Në këtë artikull do të punojmë me vazhdime, dhe do të shtyjmë monadat dhe shtypjen e paqartë për herën tjetër. Është interesante se vazhdimi është një gjë shumë e dobishme, e cila përdoret jo vetëm për të specifikuar një renditje të rreptë të llogaritjeve. Ne gjithashtu do të flasim për këtë.

Vazhdimet

Vazhdimet në programim luajnë të njëjtin rol si Kodi i Da Vinçit në historinë njerëzore: zbulimi mahnitës i misterit më të madh të njerëzimit. Epo, mbase jo fare, por ato padyshim që heqin perdet, pasi dikur mësuat të merrnit rrënjën e -1.

Kur shikuam funksionet, mësuam vetëm gjysmën e së vërtetës, sepse dolëm nga supozimi se një funksion i kthen një vlerë funksionit thirrës. Në këtë kuptim, vazhdimi është një përgjithësim i funksioneve. Funksioni nuk duhet të kthejë kontrollin në vendin nga ku është thirrur, por mund të kthehet në çdo vend të programit. Continue është një parametër që mund t'ia kalojmë një funksioni për të treguar pikën e kthimit. Tingëllon shumë më e frikshme se sa është në të vërtetë. Le të hedhim një vështrim në kodin e mëposhtëm:

Int i = shtoni (5, 10); int j = katror (i);
Funksioni add kthen numrin 15, i cili shkruhet në i, në pikën ku u thirr funksioni. Vlera e i përdoret më pas në thirrjen në katror. Vini re se përpiluesi dembel nuk mund të ndryshojë rendin e vlerësimit, sepse rreshti i dytë varet nga rezultati i të parit. Ne mund ta rishkruajmë këtë kod duke përdorur stilin e kalimit të vazhdueshëm (CPS) kur add kthen një vlerë në funksionin katror.

Int j = shtoni (5, 10, katror);
Në këtë rast, add merr një argument shtesë - një funksion që do të thirret pasi shtimi të përfundojë duke punuar. Në të dy shembujt, j do të jetë 225.

Kjo është teknika e parë që ju lejon të specifikoni rendin e ekzekutimit të dy shprehjeve. Le të kthehemi te shembulli ynë I/O.

System.out.println ("Ju lutemi shkruani emrin tuaj:"); System.in.readLine ();
Këto dy rreshta janë të pavarura nga njëra-tjetra, dhe përpiluesi është i lirë të ndryshojë rendin e tyre sipas dëshirës. Por nëse e rishkruajmë atë në CPS, atëherë duke e bërë këtë shtojmë varësinë e kërkuar dhe përpiluesi do të duhet të kryejë llogaritjet njëra pas tjetrës!

System.out.println ("Ju lutemi shkruani emrin tuaj:", System.in.readLine);
Në një rast të tillë, println do të thërriste readLine me rezultatin e tij dhe do të kthente rezultatin readLine në fund. Në këtë formë, mund të jemi të sigurt se këto funksione do të thirren me radhë dhe se readLine do të thirret fare (në fund të fundit, përpiluesi pret të marrë rezultatin e operacionit të fundit). Në rastin e Java, println kthehet në void. Por nëse do të kthehej ndonjë vlerë abstrakte (e cila mund të shërbejë si argument për readLine), atëherë kjo do të zgjidhte problemin tonë! Sigurisht, rreshtimi i zinxhirëve të tillë funksionesh dëmton shumë lexueshmërinë e kodit, por ju mund ta luftoni këtë. Ne mund të shtojmë të mira sintaksore në gjuhën tonë që do të na lejojnë të shkruajmë shprehje si zakonisht, dhe përpiluesi do t'i lidh automatikisht llogaritjet. Tani ne mund të kryejmë llogaritjet në çdo mënyrë pa humbur avantazhet e FP (përfshirë aftësinë për të studiuar programin duke përdorur metoda matematikore)! Nëse kjo ju ngatërron, mbani mend se funksionet janë vetëm shembuj të një klase me një anëtar të vetëm. Rishkruajeni shembullin tonë në mënyrë që println dhe readLine të jenë të dyja shembuj të klasës në mënyrë që ta kuptoni më mirë.

Por përfitimet e vazhdimeve nuk mbarojnë këtu. Mund të shkruajmë një program të tërë duke përdorur CPS, në mënyrë që çdo funksion të thirret me një parametër shtesë, një vazhdim, në të cilin kalohet rezultati. Në parim, çdo program mund të përkthehet në CPS, nëse mendoni për secilin funksion si një rast të veçantë vazhdimësie. Ky konvertim mund të bëhet automatikisht (në fakt, shumë përpilues e bëjnë këtë).

Sapo e konvertojmë programin në formën CPS, bëhet e qartë se çdo instruksion ka një vazhdim, funksionin në të cilin do të kalohet rezultati, i cili në një program normal do të ishte një pikë thirrjeje. Le të marrim ndonjë udhëzim nga shembulli i fundit, për shembull add (5,10). Në një program të shkruar në formë CPS, është e qartë se çfarë do të jetë një vazhdim - ky është një funksion që shtimi do të thërrasë në fund të punës. Por cili do të jetë vazhdimi i programit jo-CPS? Sigurisht, ne mund ta konvertojmë programin në CPS, por a është e nevojshme?

Rezulton se kjo nuk është e nevojshme. Shikoni nga afër konvertimin tonë të CPS. Nëse filloni të shkruani një përpilues për të, do të zbuloni se versioni CPS nuk ka nevojë për një pirg! Funksionet nuk kthejnë kurrë asgjë, në kuptimin tradicional të fjalës "kthim", ata thjesht thërrasin një funksion tjetër, duke zëvendësuar rezultatin e llogaritjeve. Nuk ka nevojë të shtyni (shtyni) argumentet në pirg përpara çdo telefonate, dhe pastaj t'i ktheni ato. Ne thjesht mund t'i ruajmë argumentet në një pjesë fikse të memories dhe të përdorim kërcimin në vend të thirrjes normale. Nuk kemi nevojë të ruajmë argumentet origjinale, sepse nuk do të na duhen më kurrë, sepse funksionet nuk kthejnë asgjë!

Kështu, programet e stilit CPS nuk kanë nevojë për një pirg, por përmbajnë një argument shtesë, në formën e një funksioni që do të thirret. Programet jo të stilit CPS nuk kanë argument shtesë, por përdorin një pirg. Çfarë ruhet në pirg? Vetëm argumente dhe një tregues për vendndodhjen e memories ku funksioni duhet të kthehet. Epo, e menduat? Rafte ruan informacione rreth vazhdimeve! Një tregues për një pikë kthimi në pirg është i njëjtë me një funksion që duhet thirrur në programet CPS! Për të zbuluar se cila shtrirje është add (5,10), mjafton të marrësh pikën e kthimit nga steka.

Nuk ishte e vështirë. Një vazhdim dhe një tregues për një pikë kthimi janë në të vërtetë e njëjta gjë, vetëm vazhdimi është specifikuar në mënyrë eksplicite, dhe për këtë arsye mund të ndryshojë nga vendi ku është thirrur funksioni. Nëse mbani mend se një vazhdim është një funksion dhe një funksion në gjuhën tonë përpilohet në një shembull të një klase, atëherë do të kuptoni se një tregues në një pikë kthimi në pirg dhe një tregues në një vazhdim janë në fakt e njëjta gjë. , sepse funksioni ynë (si shembull i një klase ) është vetëm një tregues. Kjo do të thotë që në çdo kohë në programin tuaj, ju mund të kërkoni vazhdimin aktual (në fakt, informacion nga stack).

Mirë, tani kemi një ide të mirë se çfarë është vazhdimi aktual. Çfarë do të thotë? Nëse marrim vazhdimin aktual dhe e ruajmë diku, në këtë mënyrë ruajmë gjendjen aktuale të programit - e ngrijmë atë. Kjo është e ngjashme me letargjinë e OS. Objekti i vazhdimit ruan informacionin e nevojshëm për të rifilluar ekzekutimin e programit nga pika ku u kërkua objekti i vazhdimit. Sistemi operativ e bën këtë me programet tuaja gjatë gjithë kohës kur ndërron kontekstin midis temave. Dallimi i vetëm është se gjithçka është nën kontrollin e OS. Nëse kërkoni një objekt vazhdimësie (në Skemë, kjo bëhet duke thirrur funksionin call-with-current-continuation), atëherë do të merrni një objekt me vazhdimin aktual - stivën (ose në rastin e CPS - funksionin e thirrjen tjetër). Ju mund ta ruani këtë objekt në një variabël (ose edhe në disk). Nëse zgjidhni të "rifilloni" programin me këtë vazhdim, atëherë gjendja e programit tuaj "konvertohet" në gjendjen në të cilën ishte kur u mor objekti i vazhdimit. Kjo është njësoj si kalimi në një fije të pezulluar ose zgjimi i sistemit operativ pas letargjisë. Me përjashtim që mund ta bëni këtë shumë herë radhazi. Pasi OS zgjohet, informacioni i letargji shkatërrohet. Nëse kjo nuk është bërë, atëherë do të ishte e mundur të rivendosni gjendjen e OS nga e njëjta pikë. Është pothuajse si një udhëtim në kohë. Me vazhdime, ju mund ta përballoni atë!

Në cilat situata janë të dobishme vazhdimet? Zakonisht nëse po përpiqeni të imitoni gjendjen në sisteme pa atë në thelb. Vazhdimet përdoren mirë në aplikacionet në ueb (siç është kuadri Seaside Smalltalk). ASP.NET nga Microsoft bën përpjekje të mëdha për të ruajtur gjendjen midis kërkesave dhe për ta bërë jetën tuaj më të lehtë. Nëse C # mbështet vazhdimet, kompleksiteti i ASP.NET mund të zvogëlohet në gjysmë duke ruajtur vazhdimin dhe duke e rivendosur atë në kërkesën tjetër. Nga këndvështrimi i një programuesi në internet, nuk do të kishte pushim - programi do të vazhdonte në rreshtin tjetër! Vazhdimet janë një abstraksion tepër i dobishëm për zgjidhjen e disa problemeve. Me gjithnjë e më shumë klientë tradicionalë të trashë që lëvizin në ueb, rëndësia e vazhdimeve do të rritet vetëm me kalimin e kohës.

Përputhja e modelit

Përputhja e modelit nuk është një ide aq e re apo novatore. Në fakt, ka pak të bëjë me programimin funksional. Arsyeja e vetme që shpesh lidhet me FP është se për disa kohë gjuhët funksionale kanë përputhje modelesh, por ato imperative jo.

Le të fillojmë njohjen tonë me përputhjen e modelit me shembullin e mëposhtëm. Këtu është funksioni për llogaritjen e numrave Fibonacci në Java:

Int fib (int n) (nëse (n == 0) kthen 1; nëse (n == 1) kthe 1; ktheje fib (n - 2) + fib (n - 1);)
Dhe këtu është një shembull në një gjuhë të ngjashme me Java me mbështetje për përputhjen e modelit

Int fib (0) (kthimi 1;) int fib (1) (kthimi 1;) int fib (int n) (kthimi fib (n - 2) + fib (n - 1);)
Qfare eshte dallimi? Përpiluesi bën degëzimin për ne.

Vetëm mendoni, rëndësia është e madhe! Në të vërtetë, rëndësia nuk është e madhe. U vu re se një numër i madh funksionesh përmbajnë konstruksione komplekse ndërprerëse (kjo është pjesërisht e vërtetë për programet funksionale), dhe u vendos që të theksohej kjo pikë. Përkufizimi i funksionit ndahet në disa variante dhe modeli vendoset në vend të argumenteve të funksionit (kjo është e ngjashme me mbingarkimin e metodës). Kur thirret një funksion, përpiluesi krahason argumentet kundër të gjitha përkufizimeve në lëvizje dhe zgjedh atë më të përshtatshmen. Zakonisht zgjedhja bie në përkufizimin më të specializuar të funksionit. Për shembull int fib (int n) mund të quhet kur n është 1, por jo, sepse int fib (1) është një përkufizim më i specializuar.

Përputhja e modelit është zakonisht më komplekse se shembulli ynë. Për shembull, sistemi kompleks i përputhjes së modelit ju lejon të shkruani kodin e mëposhtëm:

Int f (int n< 10) { ... } int f(int n) { ... }
Kur mund të jetë e dobishme përputhja e modeleve? Lista e rasteve të tilla është çuditërisht e gjatë! Sa herë që përdorni konstruksione komplekse të mbivendosura, përputhja e modelit mund të funksionojë më mirë me më pak kod. Një shembull i mirë vjen në mendje me funksionin WndProc, i cili zbatohet në çdo program Win32 (edhe nëse është i fshehur nga programuesi pas një gardh të lartë abstraksionesh). Në mënyrë tipike, përputhja e modeleve mund të kontrollojë edhe përmbajtjen e koleksioneve. Për shembull, nëse i kaloni një grup një funksioni, atëherë mund të zgjidhni të gjitha vargjet në të cilat elementi i parë është 1 dhe elementi i tretë është më i madh se 3.

Një avantazh tjetër i përputhjes së modelit është se nëse bëni ndryshime, nuk keni nevojë të gërmoni në një funksion të madh. Thjesht duhet të shtoni (ose të ndryshoni) disa përkufizime funksioni. Kështu, ne heqim qafe një shtresë të tërë modelesh nga libri i famshëm i Bandës së Katërve. Sa më komplekse dhe më të degëzuara të jenë kushtet, aq më i dobishëm do të jetë përputhja e modelit. Pasi të filloni t'i përdorni ato, pyesni veten se si mund të bënit pa to më parë.

Mbylljet

Deri më tani, ne kemi diskutuar veçoritë e FP në kontekstin e gjuhëve "thjesht" funksionale - gjuhë që janë zbatimi i llogaritjes lambda dhe nuk përmbajnë veçori që kundërshtojnë sistemin formal të Kishës. Megjithatë, shumë veçori të gjuhëve funksionale përdoren jashtë llogaritjes lambda. Megjithëse zbatimi i sistemit aksiomatik është interesant nga pikëpamja programuese për sa i përket shprehjeve matematikore, ai mund të mos jetë gjithmonë i zbatueshëm në praktikë. Shumë gjuhë preferojnë të përdorin elementë të gjuhëve funksionale pa iu përmbajtur një doktrine të rreptë funksionale. Disa gjuhë të tilla (për shembull Common Lisp) nuk kërkojnë që variablat të jenë përfundimtare - vlerat e tyre mund të ndryshohen. Ata as nuk kërkojnë që funksionet të varen vetëm nga argumentet e tyre - funksionet lejohen të aksesojnë gjendjen jashtë fushëveprimit të tyre. Megjithatë, ato përfshijnë gjithashtu veçori të tilla si funksione të rendit më të lartë. Kalimi i një funksioni në një gjuhë jo të pastër është paksa i ndryshëm nga operacioni analog brenda llogaritjes lambda dhe kërkon një veçori interesante të quajtur mbyllje leksikore. Le të hedhim një vështrim në shembullin e mëposhtëm. Mos harroni se në këtë rast variablat nuk janë përfundimtare dhe funksioni mund të aksesojë variablat jashtë fushëveprimit të tij:

Funksioni makePowerFn (int power) (int powerFn (int base) (turn pow (bazë, power);) return powerFn;) Function Square = makePowerFn (2); katror (3); // kthen 9
Funksioni make-power-fn kthen një funksion që merr një argument dhe e ngre atë në një fuqi specifike. Çfarë ndodh kur përpiqemi të vlerësojmë katrorin (3)? Fuqia e ndryshueshme është jashtë fushëveprimit për powerFn sepse makePowerFn tashmë ka dalë dhe grupi i tij është shkatërruar. Si funksionon atëherë katrori? Gjuha duhet të ruajë vlerën e fuqisë në një farë mënyre që funksioni katror të funksionojë. Po sikur të krijojmë një funksion tjetër kub që e ngre numrin në fuqinë e tretë? Gjuha do të duhet të ruajë dy vlera të fuqisë për çdo funksion të krijuar në make-power-fn. Fenomeni i ruajtjes së këtyre vlerave quhet mbyllje. Mbyllja bën më shumë sesa ruan argumentet e funksionit të lartë. Për shembull, një mbyllje mund të duket si kjo:

Funksioni makeIncrementer () (int n = 0; int increment () (kthim ++ n;)) Funksioni inc1 = makeIncrementer (); Funksioni inc2 = makeIncrementer (); inc1 (); // kthen 1; inc1 (); // kthen 2; inc1 (); // kthen 3; inc2 (); // kthen 1; inc2 (); // kthen 2; inc2 (); // kthen 3;
Gjatë ekzekutimit, vlerat e n ruhen dhe numëruesit kanë qasje në to. Për më tepër, çdo numërues ka kopjen e vet të n, pavarësisht nga fakti se ato duhet të ishin zhdukur pas ekzekutimit të funksionit makeIncrementer. Si arrin përpiluesi ta përpilojë këtë? Çfarë po ndodh në prapaskenat e mbylljeve? Fatmirësisht kemi një pasim magjik.

Gjithçka është bërë mjaft logjike. Në shikim të parë, është e qartë se variablat lokale nuk u binden më rregullave të fushëveprimit dhe jetëgjatësia e tyre është e papërcaktuar. Natyrisht, ato nuk ruhen më në pirg - ato duhet të mbahen në grumbull. Prandaj, mbyllja bëhet si një funksion normal që diskutuam më herët, përveç që ka një referencë shtesë për variablat përreth:

Klasa some_function_t (SymbolTable parentScope; // ...)
Nëse mbyllja i referohet një variabli që nuk është në shtrirjen lokale, atëherë ai merr parasysh shtrirjen mëmë. Kjo eshte e gjitha! Mbyllja lidh botën funksionale me botën OOP. Sa herë që krijoni një klasë që ruan ndonjë gjendje dhe e kaloni diku, mbani mend mbylljet. Një mbyllje është thjesht një objekt që krijon "atribute" në fluturim, duke i nxjerrë ato jashtë fushëveprimit, kështu që ju nuk duhet ta bëni vetë.

Tani Cfare?

Ky artikull ecën vetëm majën e ajsbergut të programimit funksional. Ju mund të gërmoni më thellë dhe të shihni diçka vërtet të madhe, dhe në rastin tonë, gjithashtu të mirë. Në të ardhmen, kam në plan të shkruaj për teorinë e kategorive, monadat, strukturat funksionale të të dhënave, sistemin e tipit në gjuhët funksionale, multithreading funksional, bazat e të dhënave funksionale dhe shumë të tjera. Nëse mund të shkruaj (dhe të mësoj gjatë rrugës) rreth gjysmës së këtyre temave, jeta ime nuk do të humbet kot. Deri atëherë, Google- miku juaj besnik.

Programimi funksional përfshin llogaritjen e rezultateve të funksioneve nga të dhënat fillestare dhe rezultatet e funksioneve të tjera, dhe nuk nënkupton një ruajtje të qartë të gjendjes së programit. Prandaj, nuk nënkupton ndryshueshmërinë e kësaj gjendje (në ndryshim nga imperativi, ku një nga konceptet bazë është një variabël që ruan vlerën e tij dhe ju lejon ta ndryshoni atë ndërsa funksionon algoritmi).

Në praktikë, ndryshimi midis një funksioni matematikor dhe konceptit të një "funksioni" në programimin imperativ është se funksionet imperative mund të mbështeten jo vetëm në argumente, por edhe në gjendjen e ndryshoreve të jashtme të funksionit, si dhe të kenë efekte anësore dhe ndryshoni gjendjen e variablave të jashtëm. Kështu, në programimin imperativ, kur thërrisni të njëjtin funksion me të njëjtat parametra, por në faza të ndryshme të ekzekutimit të algoritmit, mund të merrni të dhëna të ndryshme dalëse për shkak të efektit të gjendjes së variablave në funksion. Dhe në një gjuhë funksionale, kur një funksion thirret me të njëjtat argumente, marrim gjithmonë të njëjtin rezultat: dalja varet vetëm nga hyrja. Kjo lejon mjediset e ekzekutimit të programeve në gjuhët funksionale të ruajnë rezultatet e funksioneve dhe t'i thërrasin në një rend që nuk përcaktohet nga algoritmi dhe t'i paralelizojnë ato pa ndonjë veprim shtesë nga ana e programuesit (shih më poshtë)

Gjuhë programimi funksionale

  • LISP - (John McCarthy,) dhe shumë nga dialektet e saj, më modernet prej të cilave janë:
  • Erlang - (Joe Armstrong,) një gjuhë funksionale me mbështetje për proceset.
  • APL është pararendësi i mjediseve moderne kompjuterike shkencore si MATLAB.
  • (Robin Milner, Standard ML dhe Objective CAML njihen nga dialektet që përdoren sot).
  • - gjuha funksionale e familjes ML për platformën .NET
  • Miranda (David Turner, i cili më vonë zhvilloi gjuhën Haskell).
  • Nemerle është një gjuhë hibride funksionale/imperative.
  • Haskell është thjesht funksional. Me emrin Haskell Curry.

Versionet fillestare ende jo plotësisht funksionale të Lisp dhe APL kanë dhënë kontribut të veçantë në krijimin dhe zhvillimin e programimit funksional. Versionet e mëvonshme të Lisp si Scheme, si dhe shije të ndryshme të APL, mbështetën të gjitha vetitë dhe konceptet e një gjuhe funksionale.

Si rregull, interesi për gjuhët e programimit funksional, veçanërisht ato thjesht funksionale, ishte më shumë shkencor sesa komercial. Sidoqoftë, gjuhë të dukshme si Erlang, OCaml, Haskell, Scheme (pas 1986) dhe specifike (statistika), Mathematica (matematika simbolike) dhe K (analiza financiare) dhe XSLT (XML) gjetën rrugën e tyre në programimin komercial. industrisë. Gjuhët e përhapura deklarative të tilla si SQL dhe Lex / Yacc përmbajnë disa elementë programimi funksional, për shembull, ato janë të kujdesshme për përdorimin e variablave. Gjuhët e fletëllogaritjes mund të konsiderohen gjithashtu funksionale, sepse një sërë funksionesh specifikohet në qelizat e fletëllogaritjeve, si rregull, në varësi vetëm nga qelizat e tjera, dhe nëse doni të modeloni variabla, duhet t'i drejtoheni aftësive të gjuha makro imperative.

Histori

Gjuha e parë funksionale ishte Lisp, e krijuar nga John McCarthy gjatë kohës së tij në fund të viteve pesëdhjetë dhe u zbatua fillimisht për IBM 700/7000 (anglisht) rusisht ... Lisp prezantoi shumë nga konceptet e një gjuhe funksionale, megjithëse ajo shpalli më shumë sesa thjesht paradigmën e programimit funksional. Zhvillimi i mëtejshëm i Lisp ishin gjuhë të tilla si Scheme dhe Dylan.

Konceptet

Disa koncepte dhe paradigma janë specifike për programimin funksional dhe janë kryesisht të huaja për programimin imperativ (përfshirë programimin e orientuar nga objekti). Sidoqoftë, gjuhët e programimit janë zakonisht një hibrid i disa paradigmave programuese, kështu që gjuhët e programimit "kryesisht të domosdoshme" mund të përdorin disa nga këto koncepte.

Funksionet e rendit më të lartë

Funksionet e rendit më të lartë janë funksione që mund të marrin si argumente dhe të kthejnë funksione të tjera. Matematikanët shpesh e quajnë një funksion të tillë një operator, për shembull, operatori derivat ose operatori integral.

Funksionet e rendit më të lartë ju lejojnë të përdorni currying - transformimin e një funksioni nga një palë argumentesh në një funksion që merr argumentet e tij një nga një. Ky transformim mori emrin e tij për nder të H. Curry.

Funksione të pastra

Funksionet që nuk kanë efekte anësore të I/O dhe memorjes quhen funksione të pastra (ato varen vetëm nga parametrat e tyre dhe kthejnë vetëm rezultatin e tyre). Funksionet e pastra kanë disa veti të dobishme, shumë prej të cilave mund t'i përdorni për të optimizuar kodin tuaj:

  • Nëse rezultati i një funksioni të pastër nuk përdoret, ai mund të hiqet pa dëmtuar shprehjet e tjera.
  • Rezultati i një thirrjeje në një funksion të pastër mund të memoizohet, domethënë të ruhet në një tabelë vlerash së bashku me argumentet e thirrjes. Nëse më vonë funksioni thirret me të njëjtat argumente, rezultati i tij mund të merret drejtpërdrejt nga tabela pa u llogaritur (ndonjëherë ky quhet parimi i transparencës së referencave). Memoizimi, me koston e një konsumi të vogël memorie, mund të rrisë ndjeshëm performancën dhe të zvogëlojë rendin e rritjes së disa algoritmeve rekursive.
  • Nëse nuk ka varësi të të dhënave midis dy funksioneve të pastra, atëherë rendi i llogaritjes së tyre mund të ndryshohet ose paralelizohet (me fjalë të tjera, llogaritja e funksioneve të pastra plotëson parimet e thread-safe)
  • Nëse e gjithë gjuha nuk lejon efekte anësore, atëherë mund të përdoret çdo politikë llogaritëse. Kjo i jep kompajlerit lirinë për të kombinuar dhe riorganizuar vlerësimin e shprehjeve në program (për shembull, për të përjashtuar strukturat e pemëve).

Ndërsa shumica e përpiluesve për gjuhët e programimit imperativ njohin funksionet e pastra dhe heqin nënshprehjet e zakonshme për thirrjet e funksioneve të pastra, ata nuk mund ta bëjnë gjithmonë këtë për bibliotekat e parakompiluara, të cilat në përgjithësi nuk e japin këtë informacion. Disa përpilues, të tillë si gcc, i ofrojnë programuesit fjalë kyçe për të treguar funksione të pastra për qëllime optimizimi. Fortran 95 lejon që funksionet të përcaktohen si "të pastra".

Rekursioni

Funksionet rekursive mund të përgjithësohen me funksione të rendit më të lartë duke përdorur, për shembull, katamorfizmin dhe anamorfizmin (ose "përmbysja" dhe "shpalosja"). Funksionet e këtij lloji luajnë rolin e një gjëje të tillë si një lak në gjuhët e programimit imperativ.

Qasja e vlerësimit të argumenteve

Gjuhët funksionale mund të klasifikohen sipas mënyrës se si përpunohen argumentet e një funksioni gjatë vlerësimit të tij. Teknikisht, ndryshimi qëndron në semantikën denotative të shprehjes. Për shembull, me një qasje strikte për vlerësimin e shprehjes

Printim ([2 +1, 3 * 2, 1/0, 5 -4]))

dalja do të jetë një gabim, pasi ka ndarje me zero në elementin e tretë të listës. Me një qasje të lirë, vlera e shprehjes do të jetë 4, pasi për llogaritjen e gjatësisë së një liste, vlerat e elementeve të saj, në mënyrë rigoroze, nuk janë të rëndësishme dhe mund të mos llogariten fare. Në rendin e rreptë të llogaritjes (aplikative), vlerat e të gjitha argumenteve llogariten paraprakisht përpara se të vlerësohet vetë funksioni. Me një qasje të lirshme (rendi normal i vlerësimit), vlerat e argumenteve nuk llogariten derisa të nevojitet vlera e tyre gjatë vlerësimit të funksionit.

Si rregull, një qasje e dobët zbatohet në formën e një reduktimi grafiku. Llogaritja e cekët është parazgjedhja në disa gjuhë thjesht funksionale, duke përfshirë Miranda, Clean dhe Haskell.

FP në gjuhë jofunksionale

Në parim, nuk ka pengesa për të shkruar programe të stilit funksional në gjuhë që tradicionalisht nuk konsiderohen funksionale, ashtu si programet e orientuara nga objekti mund të shkruhen në gjuhë të strukturuara. Disa gjuhë imperative mbështesin konstruktet tipike të gjuhëve funksionale, të tilla si funksionet e rendit më të lartë dhe kuptimet e listave, duke e bërë më të lehtë përdorimin e stilit funksional në këto gjuhë. Një shembull do të ishte programimi funksional në Python.

Stilet e programimit

Programet imperative priren të theksojnë sekuencën e hapave për të kryer disa veprime, dhe programet funksionale në rregullimin dhe përbërjen e funksioneve, shpesh duke mos treguar sekuencën e saktë të hapave. Një shembull i thjeshtë i dy zgjidhjeve për të njëjtin problem (duke përdorur të njëjtën gjuhë Python) e ilustron këtë.

#stili imperativ objektiv = # krijoni një listë boshe për artikullin në listën_source: # për çdo element të listës burimore trans1 = G (artikull) # aplikoni funksionin G () trans2 = F (trans1) # aplikoni funksionin F () target.append (trans2) # shtoni artikullin e transformuar në listë

Versioni funksional duket ndryshe:

#stili funksional # Gjuhët FP shpesh kanë një funksion të integruar të kompozimit (). kompozoj2 = lambda A, B: lambda x: A (B (x)) objektiv = hartë (kompozoj2 (F, G), lista_burimi)

Ndryshe nga stili imperativ, i cili përshkruan hapat që çojnë drejt një qëllimi, stili funksional përshkruan marrëdhënien matematikore midis të dhënave dhe një qëllimi.

Veçoritë

Tipari kryesor i programimit funksional, i cili përcakton si avantazhet ashtu edhe disavantazhet e kësaj paradigme, është se ai zbaton modeli informatik pa shtetësi... Nëse një program imperativ në çdo fazë të ekzekutimit ka një gjendje, domethënë një grup vlerash të të gjitha variablave dhe prodhon efekte anësore, atëherë një program thjesht funksional nuk ka as të gjithë, as pjesë të gjendjes dhe nuk prodhon anën. efektet. Ajo që bëhet në gjuhët imperative duke u caktuar vlera variablave arrihet në gjuhët funksionale duke kaluar shprehje në parametrat e funksionit. Një pasojë e menjëhershme është se një program thjesht funksional nuk mund të ndryshojë të dhënat që ka tashmë, por mund të gjenerojë vetëm të reja duke kopjuar dhe/ose zgjeruar të vjetrat. Pasoja e së njëjtës është braktisja e sytheve në favor të rekursionit.

Pikat e forta

Përmirësimi i besueshmërisë së kodit

Ana tërheqëse e llogaritjes pa shtetësi është se kodi është më i besueshëm për shkak të strukturimit të qartë dhe mungesës së nevojës për të gjurmuar efektet anësore. Çdo funksion funksionon vetëm me të dhëna lokale dhe gjithmonë punon me to në të njëjtën mënyrë, pavarësisht se ku, si dhe në çfarë rrethanash quhet. Pamundësia e ndryshimit të të dhënave gjatë përdorimit të tyre në vende të ndryshme të programit përjashton shfaqjen e gabimeve të vështira për t'u gjetur (si p.sh., caktimi aksidental i një vlere të pasaktë në një ndryshore globale në një program imperativ).

Organizim i përshtatshëm i testimit të njësisë

Meqenëse një funksion në programimin funksional nuk mund të gjenerojë efekte anësore, objektet nuk mund të ndryshohen as brenda fushës ose jashtë (ndryshe nga programet imperative, ku një funksion mund të vendosë disa ndryshore të jashtme të lexuara nga funksioni i dytë). Efekti i vetëm i vlerësimit të një funksioni është rezultati që ai kthen dhe i vetmi faktor që ndikon në rezultat janë vlerat e argumenteve.

Kështu, është e mundur të testohet çdo funksion në një program thjesht duke e vlerësuar atë nga grupe të ndryshme vlerash argumentesh. Në këtë rast, nuk duhet të shqetësoheni për thirrjen e funksioneve në rendin e duhur, ose për formimin e saktë të gjendjes së jashtme. Nëse ndonjë funksion në një program kalon testet e njësisë, atëherë mund të jeni të sigurt në cilësinë e të gjithë programit. Në programet imperative, kontrollimi i vlerës së kthyer të një funksioni nuk mjafton: funksioni mund të modifikojë gjendjen e jashtme, e cila gjithashtu duhet të kontrollohet, gjë që nuk duhet të bëhet në programet funksionale.

Përpiloni aftësitë e optimizimit

Tipari pozitiv i përmendur tradicionalisht i programimit funksional është se ju lejon të përshkruani një program në të ashtuquajturën formë "deklarative", kur një sekuencë e ngurtë e kryerjes së shumë operacioneve të nevojshme për llogaritjen e rezultatit nuk specifikohet në mënyrë eksplicite, por gjenerohet automatikisht në procesi i llogaritjes së funksioneve. Kjo rrethanë, si dhe mungesa e gjendjeve, bën të mundur aplikimin e metodave mjaft komplekse të optimizimit automatik në programet funksionale.

Aftësitë e konkurencës

Një avantazh tjetër i programeve funksionale është se ato ofrojnë mundësi më të gjera për paralelizimin automatik të llogaritjeve. Meqenëse mungesa e efekteve anësore është e garantuar, në çdo thirrje funksioni është gjithmonë e mundur të vlerësohen paralelisht dy parametra të ndryshëm - radha në të cilën ato vlerësohen nuk mund të ndikojë në rezultatin e thirrjes.

Të metat

Disavantazhet e programimit funksional burojnë nga të njëjtat veçori. Mungesa e detyrave dhe zëvendësimi i tyre me gjenerimin e të dhënave të reja çon në nevojën për shpërndarje të vazhdueshme dhe çlirim automatik të memories, kështu që një grumbullues shumë efikas i mbeturinave bëhet një komponent i detyrueshëm në sistemin e ekzekutimit të një programi funksional. Një model i lirshëm llogaritjeje çon në renditje të paparashikueshme të thirrjeve të funksioneve, gjë që krijon probleme në I/O, ku renditja e operacioneve është e rëndësishme. Për më tepër, është e qartë se funksionet e hyrjes në formën e tyre natyrore (për shembull, getchar nga biblioteka e gjuhës standarde) nuk janë të pastra, sepse ato mund të kthejnë vlera të ndryshme për të njëjtat argumente, dhe kërkohen disa ndryshime për ta eliminuar këtë.

Për të kapërcyer mangësitë e programeve funksionale, tashmë gjuhët e para funksionale të programimit përfshinin jo vetëm mjete thjesht funksionale, por edhe mekanizma programimi imperativ (caktimi, cikli, "PROGN e nënkuptuar" ishin tashmë në LISP). Përdorimi i mjeteve të tilla ju lejon të zgjidhni disa probleme praktike, por do të thotë të largoheni nga idetë (dhe avantazhet) e programimit funksional dhe të shkruani programe imperative në gjuhë funksionale. Në gjuhët e pastra funksionale, këto probleme zgjidhen me mjete të tjera, për shembull, në Haskell, I/O zbatohet duke përdorur monadat, një koncept jo i parëndësishëm i huazuar nga teoria e kategorive.

Shiko gjithashtu

  • Anamorfizmi
  • katamorfizëm

Shënime (redakto)

  1. A. Field, P. Harrison Programimi funksional: Per. nga anglishtja - M .: Mir, 1993 .-- 637 f., Ill. ISBN 5-03-001870-0. P. 120 [Kapitulli 6: Bazat matematikore: λ-llogaritja].
  2. Indeksi i Komunitetit të Programimit Tiobe
  3. Paul Huduck (anglisht) rusisht (shtator 1989). "Konceptimi, evolucioni dhe aplikimi i gjuhëve të programimit funksional" (PDF). ACM Computing Surveys 21 (3): 359-411. DOI: 10.1145 / 72551.72554.
  4. Roger Penrose Kapitulli 2: Llogaritja Lambda e Kishës // Mendja e Re e Mbretit. Mbi kompjuterët, të menduarit dhe ligjet e fizikës = The Emperors New Mind: Concerning Computers, Minds and The Laws of Physics. - Redaksia URSS, 2003 .-- ISBN 5-354-00005-X+ ribotim ISBN 978-5-382-01266-7; 2011 r.
  5. McCarthy, John (qershor 1978). Historia e Lisp. Në Konferencën e Historisë së Gjuhëve të Programimit ACM SIGPLAN: 217-223. DOI: 10.1145 / 800025.808387.
  6. , Ch. 3. Llogaritja Lambda si gjuhë programimi
  7. Në kujtimet e tij, Herbert Simon (1991), Modelet e jetës sime fq 189-190 ISBN 0-465-04640-1 thekson se i tij, Al. Newell dhe Cliff Shaw, të cilët "shpesh quhen prindër të inteligjencës artificiale" për shkrimin e programit Logic Theorist. (anglisht) rusisht duke vërtetuar automatikisht teoremën nga Principia Mathematica (anglisht) rusisht... Për ta arritur këtë, ata duhej të krijonin një gjuhë dhe paradigmë që, në retrospektivë, mund të shihet si programim funksional.
  8. Historia e Gjuhëve të Programimit: IPL
  9. XIV. Sesioni i APL // Historia e Gjuhës së Programimit / Richard L. Wexelbblat. - Shtypi Akademik, 1981. - S. 661-693. - 749 f.
  10. Shkarkoni PDF: "Teknikat e programimit funksional, V. A. Potapenko" f. 8 "Funksionet e rendit më të lartë".
  11. GCC, Deklarimi i atributeve të funksioneve
  12. XL Fortran për AIX, V13.1> Referenca gjuhësore, Procedurat e pastra (Fortran 95)
  13. Optimizimi i thirrjeve të bishtit

Gjithmonë kam dashur të shkruaj një seri artikujsh mbi programimin funksional për këtë revistë dhe jam shumë i lumtur që më në fund pata mundësinë. Edhe pse seria ime për analizën e të dhënave nuk ka përfunduar :). Nuk do të shpall përmbajtjen e të gjithë serisë, do të them vetëm se sot do të flasim për gjuhë të ndryshme programimi që mbështesin stilin funksional dhe teknikat përkatëse të programimit.

Gjuhë programimi që jo të gjithë i dinë

Fillova të programoja që fëmijë dhe në moshën njëzet e pesë vjeç më dukej se dija dhe kuptoja gjithçka. Programimi i orientuar drejt objekteve është bërë pjesë e trurit tim, çdo libër që mund të imagjinohet për programimin industrial është lexuar. Por prapë kisha ndjenjën se më kishte munguar diçka, diçka shumë delikate dhe jashtëzakonisht e rëndësishme. Fakti është se, si shumë në vitet nëntëdhjetë, në shkollë më mësuan të programoja në Pascal (oh po, lavdi Turbo Pascal 5.5! - Ed.), Pastaj ishte C dhe C ++. Universiteti Fortran dhe më pas Java, si mjeti kryesor në punë. Dija Python dhe disa gjuhë të tjera, por gjithçka ishte e gabuar. Dhe nuk kam pasur një arsim serioz në fushën e Shkencave Kompjuterike. Një ditë, gjatë një fluturimi përtej Atlantikut, nuk mund të flija dhe doja të lexoja diçka. Disi në mënyrë magjike kisha në majë të gishtave një libër për gjuhën e programimit Haskell. Më duket se atëherë kuptova kuptimin e vërtetë të shprehjes "bukuria kërkon sakrificë".

Tani, kur njerëzit më pyesin se si e kam mësuar Haskellin, unë them kështu: në aeroplan. Ky episod ndryshoi qëndrimin tim ndaj programimit në përgjithësi. Sigurisht, pas njohjes së parë, shumë gjëra më dukeshin jo plotësisht të qarta. Më duhej të sforcohesha dhe ta studioja çështjen më thellësisht. Dhe e dini, dhjetë vjet më vonë, shumë elementë funksionalë janë bërë pjesë e gjuhëve industriale, funksionet lambda ekziston tashmë edhe në Java, konkluzioni i llojit- në C ++, përputhje modeli- në Scala. Shumë njerëz mendojnë se ky është një lloj përparimi. Dhe në këtë seri artikujsh do t'ju tregoj për teknikat e programimit funksional duke përdorur gjuhë të ndryshme dhe veçoritë e tyre.

Përdoruesit e internetit shpesh bëjnë të gjitha llojet e listave dhe majave për argëtimin e publikut. Për shembull, "një listë librash që duhet të lexoni deri në moshën tridhjetë". Nëse detyra ime do të ishte të bëja një listë librash mbi programimin që duhet t'i lexosh derisa të kthehesh pak atje, atëherë vendi i parë sigurisht që do t'i takonte librit të Abelson dhe Sassman. "Struktura dhe interpretimi i programeve kompjuterike"... Ndonjëherë edhe më duket se përpiluesi ose interpretuesi ndonjë gjuha duhet të ndalojë këdo që nuk e ka lexuar këtë libër.

Pra, nëse ka një gjuhë me të cilën të filloni të mësoni për programimin funksional, ajo është Lisp. Në përgjithësi, kjo është një familje e tërë gjuhësh, e cila përfshin një gjuhë që është mjaft e njohur tani për JVM të quajtur Clojure... Por si gjuha e parë funksionale, ajo nuk është veçanërisht e përshtatshme. Për këtë është më mirë të përdoret gjuha Skema, e cila u zhvillua në MIT dhe deri në mesin e viteve 2000 shërbeu si gjuha kryesore për mësimdhënien e programimit. Edhe pse kursi hyrës me të njëjtin titull si libri në fjalë tani është zëvendësuar nga një kurs Python, ai është ende i rëndësishëm.

Do të përpiqem të flas shkurtimisht për gjuhën e skemës dhe, në përgjithësi, për idenë që qëndron pas gjuhëve të këtij grupi. Përkundër faktit se Lisp është shumë i vjetër (nga të gjitha gjuhët e nivelit të lartë, vetëm Fortran është më i vjetër), ishte në të që shumë nga metodat e programimit të përdorura sot u bënë të disponueshme për herë të parë. Në vijim, unë do të përdor emrin Lisp për t'iu referuar një zbatimi specifik, Skemës.

Sintaksa në dy minuta

Sintaksa në Lisp është, uh, pak e diskutueshme. Fakti është se ideja pas sintaksës është jashtëzakonisht e thjeshtë dhe është ndërtuar mbi bazën e të ashtuquajturës Shprehjet S... Ky është një shënim parashtesor në të cilin shprehja 2 + 3 me të cilën jeni mësuar shkruhet si (+ 2 3). Mund të duket e çuditshme, por në praktikë jep disa mundësi shtesë. Nga rruga, (+ 2 10 (* 3.14 2)) gjithashtu funksionon :). Kështu, i gjithë programi është një koleksion listash që përdorin shënimin e prefiksit. Në rastin e Lisp, vetë programi dhe pema e sintaksës abstrakte - "nëse e dini se çfarë dua të them" - në thelb nuk ndryshojnë. Ky shënim e bën shumë të lehtë analizimin e programeve Lisp.
Meqenëse po flasim për një gjuhë programimi, atëherë duhet thënë se si të përcaktohen funksionet në këtë gjuhë.

Këtu duhet të bëjmë një digresion të vogël. Ekziston një hollësi, rëndësia e së cilës nënvlerësohet në letërsinë moderne. Gjithsesi është e nevojshme të veçojmë funksionin në kuptimin matematikor dhe funksionin, siç e kuptojmë në programimin funksional. Fakti është se në matematikë funksionet janë objekte deklarative, ndërsa në programim ato përdoren për të organizuar procesin e llogaritjeve, domethënë në një farë kuptimi janë njohuri më tepër imperative, njohuri që i përgjigjen pyetjes "si?" Kjo është arsyeja pse Abelson dhe Sussman në librin e tyre e ndajnë me shumë kujdes këtë dhe thërrasin funksione në procedurat e programimit. Kjo nuk pranohet në literaturën moderne të programimit funksional. Por unë ende rekomandoj fuqimisht që t'i ndani këto dy kuptime të fjalës "funksion" të paktën në kokën tuaj.

Mënyra më e lehtë për të përcaktuar një funksion është të shkruani kodin e mëposhtëm. Le të fillojmë me të thjeshtën e turpshme:

(përcaktoni (rrënjët katrore a b c) (le të ((D (- (* b b) (* 4 a c)))) (nëse (< D 0) (list) (let ((sqrtD (sqrt D))) (let ((x1 (/ (- (- b) sqrtD) (* 2.0 a))) (x2 (/ (+ (- b) sqrtD) (* 2.0 a)))) (list x1 x2))))))

Po, kjo është pikërisht ajo që keni menduar - zgjidhja e një ekuacioni kuadratik në Skemë. Por kjo është më se e mjaftueshme për të dalluar të gjitha tiparet e sintaksës. Këtu sq-roots është emri i një funksioni nga tre parametra formalë.

Në pamje të parë, ka shumë kllapa në konstruktin let që përdoret për të përcaktuar variablat lokale. Por nuk është kështu, ne fillimisht përcaktojmë listën e variablave dhe më pas shprehjen në të cilën përdoren këto variabla. Këtu (lista) është një listë boshe që e kthejmë kur nuk ka rrënjë, dhe (lista x1 x2) është një listë me dy vlera.

Tani për shprehjet. Në funksionin tonë sq-roots, ne përdorëm konstruktin if. Këtu hyn programimi funksional.

Çështja është se, ndryshe nga gjuhët urdhërore si C, në gjuhët funksionale, nëse është një shprehje, jo një operator. Në praktikë, kjo do të thotë se nuk mund të ketë një degë tjetër. Sepse shprehja duhet gjithmonë të ketë rëndësi.

Nuk mund të flasësh për sintaksë pa folur sheqer sintaksor... Në gjuhët e programimit, sheqeri sintaksor quhet konstruksione që nuk janë të nevojshme, por vetëm e bëjnë kodin më të lehtë për t'u lexuar dhe ripërdorur. Le të fillojmë me një shembull klasik nga gjuha C. Shumë njerëz e dinë se vargjet nuk janë një mjet i kërkuar shprehës, pasi ka tregues. Po, me të vërtetë, vargjet zbatohen përmes treguesve, dhe një [i] për gjuhën C është e njëjtë me * (a + i). Ky shembull është përgjithësisht mjaft i pazakontë, me një efekt qesharak të lidhur me të: meqenëse operacioni i mbledhjes mbetet komutativ në rastin e treguesve, shprehja e fundit është e njëjtë me * (i + a), dhe kjo mund të merret duke hequr sheqerin sintaksor nga shprehjet i [a]! Operacioni për heqjen e sheqerit sintaksor në anglisht quhet një fjalë e veçantë heqja e sheqerit.

Duke iu rikthyer gjuhës së skemës, ekziston një shembull i rëndësishëm i sheqerit sintaksor. Për të përcaktuar variablat, si në rastin e funksioneve, përdoret fjala kyçe (në Lisp dhe Scheme kjo quhet formë speciale) define. Për shembull, (përcaktoni pi 3.14159) përcakton variablin pi. Në përgjithësi, ju mund të përcaktoni funksionet në të njëjtën mënyrë:

(përcaktoni katrorin (lambda (x) (* x x)))

kjo është njësoj si

(përcaktoni (katror x) (* x x))

Rreshti i fundit duket pak më i lexueshëm në krahasim me shprehjen lambda. Sidoqoftë, është e qartë se mjafton të kesh opsionin e parë, dhe i dyti është fakultativ. Pse e para është më e rëndësishme? Sepse një nga vetitë më themelore të gjuhëve funksionale është se funksionet në to janë objekte të klasit të parë. Kjo e fundit do të thotë që funksionet mund të kalohen si argument dhe të kthehen si vlerë.

Nëse shikoni letrën nga këndvështrimi i një shprehje lambda, mund të shihni lehtësisht korrespondencën e mëposhtme:

(le ((x 5) (y 2)) (* x y)) (zbatohet (lambda (x y) (* x y)) (lista 5 2))

Programim funksional

Gjuhët funksionale janë pastër dhe i papastër... Gjuhët e pastra funksionale janë relativisht të rralla, këto përfshijnë kryesisht Haskell dhe I pastër... Nuk ka efekte anësore në gjuhët e pastra. Në praktikë, kjo do të thotë që nuk ka asnjë detyrë dhe I/O në formën që jemi mësuar. Kjo krijon një sërë vështirësish, megjithëse në gjuhët e përmendura tashmë kjo zgjidhet mjaft zgjuar dhe në këto gjuhë ata shkruajnë kodin me shumë I/O. Gjuhët si Lisp, OCaml ose Scala lejojnë funksione me efekte anësore, dhe në këtë kuptim, këto gjuhë janë shpesh më praktike.

Detyra jonë është të mësojmë teknikat bazë të programimit funksional në Skemë. Prandaj, ne do të shkruajmë kodin thjesht funksional, pa përdorur gjeneratorin e numrave të rastësishëm, I / O dhe funksionin e vendosur! , i cili do të lejojë ndryshimin e vlerave të variablave. Ju mund të lexoni për të gjitha këto në libër. SICP... Tani le të ndalemi te më thelbësorja për ne.

Gjëja e parë që ngatërron një fillestar në programimin funksional është mungesa e sytheve. Por çfarë lidhje me? Shumë prej nesh janë mësuar se rekursioni është i keq. Kjo argumentohet nga fakti se rekursioni në gjuhët konvencionale të programimit zakonisht zbatohet në mënyrë joefikase. Çështja është se, në rastin e përgjithshëm, duhet bërë dallimi midis rekursionit si teknikë, domethënë thirrjes së një funksioni nga vetvetja, dhe rekursionit si proces. Gjuhët funksionale mbështesin optimizimin e rekursionit të bishtit, ose, siç thonë ndonjëherë, rekursionin e akumuluesit. Kjo mund të ilustrohet me një shembull të thjeshtë.

Le të kemi dy funksione - succ dhe prev. I pari kthen një numër 1 më shumë se argumenti, dhe i dyti kthen 1 më pak. Tani le të përpiqemi të përcaktojmë operacionin e shtimit në dy mënyra:

(përkufizoni (shtoni xy) (nëse (eq? y 0) x (shtoni (succ x) (para y)))) (përcaktoni (shtoni-1 xy) (nëse (baraz? y 0) x (succ (shtoj- 1 x (viti i mëparshëm)))))

Cili është ndryshimi midis rastit të parë dhe të dytë? Fakti është se nëse marrim parasysh metodën e llogaritjes për rastin e parë në hapa, atëherë mund të shihni sa vijon:

(shtoni 3 4) => (shtoni 4 3) => (shtoni 5 2) => (shtoni 6 1) => (shtoni 7 0) => 7

Në rastin e dytë, do të kemi diçka si më poshtë:

(shtoj-1 3 4) => (succ (shtoj-1 3 3)) => (succ (shtoj (shtoj-1 3 2))) => (succ (succ (succ (shtoj-1 3 1)) )) => (succ (succ (succ (succ (shtoj-1 3 0))))) => (succ (succ (succ (succ (succ 3)))) => (succ (succ (succ (succ 4))) => (succ (succ 5)) => (succ 6) => 7

Përkundër faktit se në të dyja rastet rezultati është i njëjtë, procesi i llogaritjes është thelbësisht i ndryshëm. Në rastin e parë, sasia e memories së përdorur nuk ndryshon, dhe në të dytën rritet në mënyrë lineare. Procesi i parë është përsëritëse, dhe e dyta - rekursive... Pra, për të shkruar programe efikase në gjuhë funksionale, duhet të përdorni rekursionin e bishtit në mënyrë që të shmangni tejmbushjen e stivit.

Listat

Një nga elementët më të rëndësishëm të programimit funksional, së bashku me rekursionin, është listat... Ato ofrojnë bazën për strukturat komplekse të të dhënave. Ashtu si me gjuhët e tjera funksionale, listat janë të lidhura veçmas në një mënyrë kokë e bisht. Funksioni kundër përdoret për të krijuar listën, dhe funksionet e makinës dhe cdr përdoren për të aksesuar përkatësisht kokën dhe bishtin e listës. Pra, lista (lista 1 2 3) nuk është gjë tjetër veçse (kundër 1 (kundër 2 (kundër 3 "()))). Këtu" () është një listë boshe. Kështu, një funksion tipik i përpunimit të listës duket si ky:

(përcaktoni (shumën lst) (nëse (nul? lst) 0 (+ (makina lst) (shuma (cdr lst)))))

Ky funksion thjesht përmbledh elementet e listës. Kështu duken shumë funksione të përpunimit të listës, në një artikull të ardhshëm do të shpjegoj pse. Tani për tani, vetëm vini re se nëse zëvendësojmë argumentin e parë shtesë me 1, marrim një funksion që llogarit gjatësinë e listës.

Funksionet e rendit më të lartë

Meqenëse funksionet mund të kalohen si argumente dhe të kthehen si vlera, është një ide e mirë të gjesh një përdorim për këtë. Merrni parasysh shembullin klasik të mëposhtëm:

(përcaktoni (hartën f lst) (nëse (null? lst) lst (kundër (f (makina lst)) (harta f (cdr lst)))))

Funksioni i hartës zbaton funksionin f për çdo artikull në listë. Sado e çuditshme që mund të duket, tani ne mund të shprehim funksionin për llogaritjen e gjatësisë së listës në kuptim të shumës dhe hartës:

(përcaktoni (gjatësia lst) (shuma (harta (lambda (x) 1) lst)))

Nëse papritmas keni vendosur që e gjithë kjo është disi shumë e thjeshtë, atëherë le të mendojmë për këtë: si të bëjmë një zbatim të listave duke përdorur funksione të rendit më të lartë?

Kjo do të thotë, ju duhet të zbatoni funksionet cons, car dhe cdr në mënyrë që ato të plotësojnë marrëdhënien e mëposhtme: për çdo listë lst, është e vërtetë që vlera (cons (car lst) (cdr lst)) është e njëjtë me lst . Kjo mund të bëhet si më poshtë:

(përcaktoni (kundër x xs) (lambda (zgjedh) (nëse (eq? zgjidhni 1) x xs))) (përcaktoni (makinë f) (f 1)) (përcaktoni (cdr f) (f 2))

Si punon? Këtu, funksioni kundër kthen një funksion tjetër që ka një parametër dhe, në varësi të tij, kthen ose argumentin e parë ose të dytë. Është e lehtë të kontrollohet nëse raporti i kërkuar është i kënaqur për këto funksione.

Duke përdorur kuotën dhe metaprogramimin

Një veçori e këndshme e gjuhës Lisp e bën atë jashtëzakonisht të përshtatshme për të shkruar programe që konvertojnë programe të tjera. Çështja është se një program përbëhet nga lista, dhe një listë është struktura kryesore e të dhënave në gjuhë. Ekziston një mënyrë për të "cituar" thjesht tekstin e programit në mënyrë që ai të perceptohet si një listë atomesh.

Atomet janë thjesht shprehje karakteresh, për shembull (botë "hello"), që është e njëjtë me "(hello world), ose në formë të plotë (citim (hello world)). Edhe pse shumica e dialekteve Lisp kanë vargje , ndonjëherë mund t'ia dalësh me quote. Më e rëndësishmja, kjo qasje mund të thjeshtojë gjenerimin e kodit dhe përpunimin e programit.

Së pari, le të përpiqemi të kuptojmë llogaritjet simbolike. Zakonisht, kjo kuptohet si një sistem kompjuterik algjebër që është në gjendje të trajtojë objekte simbolike, formula, ekuacione dhe objekte të tjera komplekse matematikore (ka shumë sisteme të tilla, shembujt kryesorë janë sistemet Panje dhe Matematika).

Mund të përpiqeni të zbatoni diferencimin simbolik. Unë mendoj se të gjithë ata që janë afër mbarimit të shkollës së mesme imagjinojnë rregullat e diferencimit (edhe pse në realitet gjithçka është pak më e ndërlikuar - këtu do të llogarisim derivatin e pjesshëm, thjesht duke i konsideruar variablat e tjerë si konstante, por kjo nuk e ndërlikon thelbin e çështjes fare).

Kështu që unë do t'ju jap vetëm një kod shembull që do të tregonte thelbin e çështjes, duke ia lënë detajet lexuesit (i cili, shpresoj, do të studiojë me kujdes librin "Struktura dhe interpretimi i programeve kompjuterike").

(përcaktoni (deriv exp var) (kond ((number? exp) 0) ((variable? exp) (nëse (same-variable? exp var) 1 0)) ((sum? exp) (make-sum (deriv ( addend exp) var) (deriv (augend exp) var))) ((produkt? exp) (make-sum (make-product (multiplier exp) (deriv (multiplicand exp) var)) (make-product (deriv (shumzues exp) var) (shumëzimi dhe exp)))) (tjetër (gabim "lloji i panjohur i shprehjes - DERIV" exp))))

Këtu deriv i funksionit është zbatimi i algoritmit të diferencimit siç bëhet në shkollë. Ky funksion kërkon zbatimin e numrit? , variabël? e kështu me radhë, të cilat bëjnë të mundur të kuptojmë se çfarë natyre ka ky apo ai element shprehjeje. Ju gjithashtu duhet të zbatoni funksione shtesë make-product dhe make-sum. Këtu, ne përdorim kondicionin e ndërtimit, i cili është ende i panjohur për ne, i cili është një analog i deklaratës switch në gjuhët e programimit si C dhe Java.

Përpara se të kalojmë në zbatimin e veçorive që mungojnë, vlen të përmendet se programimi funksional shpesh përdor një qasje nga lart-poshtë për zhvillimin. Kjo është kur funksionet më të përgjithshme shkruhen së pari, dhe më pas funksionet e vogla përgjegjëse për detajet e zbatimit.

(përcaktoni (ndryshore? x) (simbol? x)) (përcaktoni (të njëjtën variabël? v1 v2) (dhe (ndryshore? v1) (ndryshore? v2) (eq? v1 v2))) (përcaktoni (përcaktoni shumën a1 a2) (lista "+ a1 a2)) (përcaktoni (produktin e prodhimit m1 m2) (listën" * m1 m2)) (përcaktoni (shumën? x) (dhe (çift? x) (eq? (makinë x) "+ ))) (përcaktoni (shtoni s) (cadr s)) (përcaktoni (augend s) (caddr s)) (përcaktoni (produkt? x) (dhe (çift? x) (eq? (makinë x) "*)) ) (përcaktoni (shumëzues p) (cadr p)) (përcaktoni (shumëzues p) (caddr p))

Zbatimi i këtyre funksioneve nuk kërkon komente të veçanta, me përjashtim të mundshëm të funksioneve cadr dhe caddr. Këto nuk janë gjë tjetër veçse funksione që kthejnë përkatësisht elementin e dytë dhe të tretë të listës.

Nëse përdorni interpretuesin interaktiv të skemës, mund të siguroheni lehtësisht që kodi që rezulton funksionon si duhet, por pa thjeshtuar shprehjet:

(deriv "(+ x 3)" x) => (+ 1 0) (deriv "(* (* xy) (+ x 3))" x) => (+ (* (* xy) (+ 1 0 )) (* (+ (* x 0) (* 1 y)) (+ x 3)))

Për raste të parëndësishme (për shembull, shumëzimi me 0), problemi i thjeshtimit është mjaft i lehtë për t'u zgjidhur. Kjo pyetje i lihet lexuesit. Shumica e shembujve në këtë artikull janë marrë nga libri SICP, kështu që në rast vështirësish thjesht mund t'i referoheni burimit (libri është në domenin publik).

Si çdo dialekt, Lisp ka aftësi të mëdha metaprogramimi, kryesisht të lidhura me përdorimin e makrove. Fatkeqësisht, kjo çështje kërkon një artikull të veçantë.

Le të shkruajmë një funksion që heq sheqerin sintaksor nga përkufizimi i funksionit siç u diskutua më parë:

(përcakto (desugar-define def) (le ((fn-args (cadr def)) (trup (caddr def))) (((emri (makinë fn-args)) (args (cdr fn-args))) (lista "përcaktoni emrin (listën" trupi lambda args)))))

Ky funksion funksionon shkëlqyeshëm me përkufizime të mirëformuara të funksioneve:

(desugar-define "(define (succ x) (+ x 1))) => (define succ (lambda (x) (+ x 1)))

Megjithatë, kjo nuk funksionon për përkufizime normale si (përcaktoni x 5).
Nëse duam të heqim sheqerin sintaksor në një program të madh që përmban shumë përkufizime të ndryshme, atëherë duhet të zbatojmë një kontroll shtesë:

(përkufizoni (e sheqerosur? def) (dhe (eq? (def makinë) "përcaktoni) (lista? (cadr def))))

Një kontroll i tillë mund të ndërtohet drejtpërdrejt në funksionin desugar-define, duke e bërë atë në mënyrë që nëse përkufizimi nuk ka nevojë të heqë sheqerin sintaksor, ai thjesht nuk ndryshon (ky ushtrim i parëndësishëm i lihet lexuesit). Pastaj mund ta mbështillni të gjithë programin në një listë dhe të përdorni hartën:

(harta desugar-define prog)

konkluzioni

Në këtë artikull, nuk i vura vetes detyrën të tregoja për Skemën në ndonjë detaj. Para së gjithash, doja të tregoja disa veçori interesante të gjuhës dhe të tërhiqja lexuesin në studimin e programimit funksional. Kjo gjuhë e mrekullueshme, me gjithë thjeshtësinë e saj, ka hijeshinë dhe veçoritë e saj që e bëjnë programimin në të shumë argëtues. Sa i përket mjetit për të punuar me Skemën, të fortët në shpirt mund të lëvizin MIT-Skema dhe pjesa tjetër - përdorni mjedisin e shkëlqyer të të mësuarit Dr. Raketa... Në një nga artikujt e mëposhtëm, unë patjetër do t'ju tregoj se si të shkruani përkthyesin tuaj të skemës.

Llogaritje dembele

Në gjuhët tradicionale të programimit (të tilla si C ++), një thirrje funksioni vlerëson të gjitha argumentet. Kjo metodë e thirrjes së një funksioni quhet thirrje për vlerë. Nëse ndonjë argument nuk është përdorur në funksion, atëherë rezultati i llogaritjeve humbet, prandaj, llogaritjet janë humbur. Në një farë kuptimi, e kundërta e thirrjes për vlerë është thirrje për vlerë. Në këtë rast, argumenti vlerësohet vetëm nëse nevojitet për të llogaritur rezultatin. Një shembull i një sjelljeje të tillë është operatori lidhor, i gjithë nga i njëjti C ++ (&&), i cili nuk llogarit vlerën e argumentit të dytë nëse argumenti i parë është fals.

Nëse një gjuhë funksionale nuk mbështet llogaritjen dembel, atëherë ajo quhet strikte. Në fakt, në gjuhë të tilla, radha e vlerësimit është e përcaktuar rreptësisht. Shembuj të gjuhëve strikte përfshijnë Scheme, Standard ML dhe Caml.

Gjuhët që përdorin vlerësim dembel quhen lax. Haskell është një gjuhë e lirshme, ashtu si Gofer dhe Miranda, për shembull. Gjuhët e dobëta janë shpesh të pastra.

Shumë shpesh, gjuhët strikte përfshijnë një mjet për të mbështetur disa veçori të dobishme të qenësishme në gjuhët jo strikte, të tilla si listat e pafundme. Standardi ML vjen me një modul të veçantë për të mbështetur llogaritjen e shtyrë. Dhe Objektivi Caml gjithashtu mbështet fjalën shtesë të rezervuar dembel dhe ndërton për listat e nevojshme të vlerave.

Ky seksion ofron një përshkrim të shkurtër të disa (shumë pak) gjuhë programimi funksionale.

§ Lisp(Procesori i listës). Konsiderohet si gjuha e parë funksionale e programimit. E pashtypshme. Ai përmban shumë veti imperative, por në përgjithësi inkurajon stilin funksional të programimit. Përdor thirrje për vlerë në llogaritje. Ekziston një dialekt i orientuar nga objekti i gjuhës - CLOS.

§ ISWIM(Nëse e shihni atë që dua të them). Një gjuhë prototip funksionale. Zhvilluar nga Landin në vitet '60 të shekullit XX për të demonstruar se çfarë mund të jetë një gjuhë programimi funksionale. Së bashku me gjuhën, Landin zhvilloi gjithashtu një makinë virtuale të veçantë për ekzekutimin e programeve në ISWIM. Kjo makinë virtuale thirrje për vlerë quhet makina SECD. Sintaksa e gjuhës ISWIM është baza për sintaksën e shumë gjuhëve funksionale. Sintaksa e ML është e ngjashme me sintaksën ISWIM, veçanërisht Caml.

§ Skema... Një dialekt Lisp i destinuar për kërkime shkencore në fushën e shkencës kompjuterike. Gjatë zhvillimit të Skemës, theksi u vu në elegancën dhe thjeshtësinë e gjuhës. Kjo e bën gjuhën shumë më të vogël se Common Lisp.


§ ML(Gjuha Meta). Një familje e gjuhëve strikte me një sistem të avancuar të tipit polimorfik dhe module të parametrizueshme. ML mësohet në shumë universitete perëndimore (disa edhe si gjuha e parë e programimit).

§ ML standarde... Një nga gjuhët e para të programimit funksional të shtypur. Përmban disa veti imperative, të tilla si referenca ndaj vlerave të ndryshueshme, dhe për këtë arsye nuk është i pastër. Përdor thirrje për vlerë në llogaritje. Një zbatim shumë interesant i modularitetit. Sistem i fuqishëm i tipit polimorfik. Standardi më i fundit i gjuhës është standardi ML-97, për të cilin ka përkufizime formale matematikore të sintaksës, si dhe semantikë statike dhe dinamike të gjuhës.

§ Dritë Caml dhe Objektivi Caml... Ashtu si Standard ML i përket familjes ML. Objektivi Caml ndryshon nga Caml Light kryesisht në mbështetjen e tij për programimin klasik të orientuar nga objekti. Ngjashëm me Standard ML, ai është i rreptë, por ka një mbështetje të integruar për vlerësimin dembel.

§ Miranda... Zhvilluar nga David Turner si një gjuhë standarde funksionale duke përdorur llogaritjen dembel. Ka një sistem të rreptë të tipit polimorfik. Ashtu si ML, ajo mësohet në shumë universitete. Ai pati një ndikim të madh te zhvilluesit e gjuhës Haskell.

§ Haskell... Një nga gjuhët më të dobëta. Ka një sistem shumë të zhvilluar të shtypjes. Sistemi i moduleve është disi më pak i zhvilluar. Standardi më i fundit i gjuhës është Haskell-98.

§ Gofer(Mirë për arsyetimin ekuacion). Një dialekt i thjeshtuar i Haskell. Projektuar për mësimin e programimit funksional.

§ I pastër... Projektuar posaçërisht për programim paralel dhe të shpërndarë. Sintaksa është e ngjashme me Haskell. I pastër. Përdor llogaritje dembele. Përpiluesi vjen me një grup bibliotekash I/O që ju lejojnë të programoni një ndërfaqe grafike të përdoruesit nën Win32 ose MacOS.

Kujtojmë se karakteristika më e rëndësishme e qasjes funksionale është fakti se çdo program i zhvilluar në një gjuhë programimi funksional mund të konsiderohet si funksion, argumentet e të cilit, ndoshta, janë edhe funksione.

Qasja funksionale lindi një familje të tërë gjuhësh, paraardhësi i së cilës, siç u përmend tashmë, ishte gjuha e programimit LISP. Më vonë, në vitet '70, u zhvillua versioni origjinal i gjuhës ML, i cili më pas u zhvillua, në veçanti, në SML, si dhe një numër gjuhësh të tjera. Nga këto, ndoshta "më e reja" është gjuha Haskell, e krijuar kohët e fundit, në vitet '90.

Një avantazh i rëndësishëm i zbatimit të gjuhëve funksionale të programimit është shpërndarja e automatizuar dinamike e kujtesës së kompjuterit për ruajtjen e të dhënave. Në të njëjtën kohë, programuesi heq qafe nevojën për të kontrolluar të dhënat dhe, nëse është e nevojshme, mund të ekzekutojë funksionin "grumbullimi i mbeturinave" - ​​duke pastruar kujtesën e atyre të dhënave që programit nuk i nevojiten më.

Në qasjen funksionale, programet komplekse ndërtohen duke grumbulluar funksione. Në këtë rast, teksti i programit është një funksion, disa nga argumentet e të cilit mund të konsiderohen edhe si funksione. Kështu, ripërdorimi i kodit reduktohet në thirrjen e funksionit të përshkruar më parë, struktura e të cilit, në ndryshim nga procedura e gjuhës imperative, është matematikisht transparente.

Meqenëse funksioni është një formalizëm i natyrshëm për gjuhët funksionale të programimit, zbatimi i aspekteve të ndryshme të programimit që lidhen me funksionet është thjeshtuar shumë. Shkrimi i funksioneve rekursive bëhet intuitivisht transparent, d.m.th. funksione që e quajnë veten si argument. Gjithashtu bëhet e natyrshme zbatimi i përpunimit të strukturave rekursive të të dhënave.

Për shkak të zbatimit të mekanizmit të përputhjes së modelit, gjuhët funksionale të programimit si ML dhe Haskell janë të mira për përpunim simbolik.

Natyrisht, gjuhët funksionale të programimit nuk janë pa disa të meta.

Shpesh këto përfshijnë një strukturë programi jolineare dhe efikasitet relativisht të ulët të zbatimit. Sidoqoftë, pengesa e parë është mjaft subjektive, dhe e dyta është kapërcyer me sukses nga zbatimet moderne, në veçanti, nga një numër i përkthyesve më të fundit të gjuhës SML, duke përfshirë një përpilues për mjedisin Microsoft .NET.

Zhvillimi i softuerit profesional në gjuhët funksionale të programimit kërkon një kuptim të thellë të natyrës së një funksioni.

Vini re se termi "funksion" në formalizimin matematikor dhe zbatimin e softuerit nënkupton koncepte të ndryshme.

Kështu, një funksion matematikor f me një domen të përkufizimit A dhe një gamë vlerash B është grupi i çifteve të renditura

të tillë që nëse

(a, b 1) f dhe (a, b 2) f,

Nga ana tjetër, një funksion në një gjuhë programimi është një ndërtim i kësaj gjuhe që përshkruan rregullat për konvertimin e një argumenti (i ashtuquajturi parametri aktual) në një rezultat.

Për të zyrtarizuar konceptin e "funksionit", u ndërtua një teori matematikore, e njohur si llogaritja lambda. Më saktësisht, kjo llogaritje duhet të quhet llogaritja e shndërrimeve të lambda.

Konvertimi kuptohet si shndërrimi i objekteve të llogaritjes (dhe në programim - funksioneve dhe të dhënave) nga një formë në tjetrën. Sfida fillestare në matematikë ishte përpjekja për të thjeshtuar formën e shprehjeve. Në programim, ky problem i veçantë nuk është aq thelbësor, megjithëse, siç do të shohim më vonë, përdorimi i llogaritjes lambda si një zyrtarizim fillestar mund të ndihmojë në thjeshtimin e llojit të programit, d.m.th. çojnë në optimizimin e kodit të programit.

Për më tepër, konvertimet sigurojnë një kalim në emërtimet e prezantuara rishtazi dhe, në këtë mënyrë, lejojnë përfaqësimin e fushës së temës në një formë më kompakte ose më të detajuar, ose, në terma matematikorë, për të ndryshuar nivelin e abstraksionit në lidhje me zonën e temës. Kjo mundësi përdoret gjithashtu gjerësisht nga gjuhët e programimit të orientuar drejt objektit dhe strukturor modular në hierarkinë e objekteve, fragmenteve të programit dhe strukturave të të dhënave. Në të njëjtin parim bazohet edhe ndërveprimi i komponentëve të aplikacionit në .NET. Është në këtë kuptim që kalimi në emërtime të reja është një nga elementët më të rëndësishëm të programimit në përgjithësi, dhe është llogaritja lambda (në ndryshim nga shumë degë të tjera të matematikës) që është një mënyrë adekuate për të zyrtarizuar ripërcaktimet.

Le të sistematizojmë evolucionin e teorive që qëndrojnë në themel të qasjes moderne ndaj llogaritjes lambda.

Konsideroni evolucionin e gjuhëve të programimit që zhvillohen brenda kornizës së qasjes funksionale.

Gjuhët e hershme të programimit funksional, të cilat e kanë origjinën nga gjuha klasike LISP (LISt Processing), janë krijuar për të përpunuar listat, d.m.th. informacion simbolik. Në të njëjtën kohë, llojet kryesore ishin një element atomik dhe një listë e elementeve atomike, dhe theksi kryesor ishte në analizën e përmbajtjes së listës.

Zhvillimi i gjuhëve të hershme të programimit ishte gjuhë programimi funksionale me shtypje të fortë, një shembull tipik këtu është ML klasik, dhe pasardhësi i tij i drejtpërdrejtë SML. Në gjuhët e shtypura fort, çdo konstrukt (ose shprehje) duhet të ketë një lloj.

Megjithatë, në gjuhët e programimit funksional të mëvonshëm, nuk ka nevojë për caktimin e tipit të qartë dhe llojet e shprehjeve fillimisht të papërcaktuara, si në SML, mund të konkludohen (para se të fillojë programi) bazuar në llojet e shprehjeve që lidhen me to.

Hapi tjetër në zhvillimin e gjuhëve funksionale të programimit ishte mbështetja e funksioneve polimorfike, d.m.th. funksionet me argumente parametrike (analoge të një funksioni matematikor me parametra). Në veçanti, polimorfizmi mbështetet në gjuhët SML, Miranda dhe Haskell.

Në fazën aktuale të zhvillimit, gjuhët funksionale të programimit të "gjeneratës së re" janë shfaqur me karakteristikat e mëposhtme të avancuara: përputhjen e modelit (Skema, SML, Miranda, Haskell), polimorfizmi parametrik (SML) dhe i ashtuquajturi "dembel". " (sipas nevojës) llogaritja (Haskell, Miranda, SML).

Familja e gjuhëve të programimit funksional është mjaft e shumtë. Kjo dëshmohet jo aq nga lista e rëndësishme e gjuhëve, sa nga fakti që shumë gjuhë kanë krijuar drejtime të tëra në programim. Kujtojmë që LISP ka krijuar një familje të tërë gjuhësh: Scheme, InterLisp, COMMON Lisp, etj.

Gjuha e programimit SML nuk ishte përjashtim, e cila u krijua në formën e ML nga R. Milner në MIT (Instituti i Teknologjisë në Masachusetts) dhe fillimisht ishte menduar për konkluzionet logjike, në veçanti për vërtetimin e teoremës. Gjuha është e shtypur fort dhe i mungon polimorfizmi parametrik.

Tre gjuhë moderne me aftësi praktikisht identike (polimorfizëm parametrik, përputhje modeli, llogaritje "dembele") u bënë zhvillimi i ML "klasike". Kjo është gjuha SML e zhvilluar në MB dhe SHBA, CaML, e krijuar nga një grup shkencëtarësh francezë në Institutin INRIA, SML / NJ është një dialekt SML nga New Jersey, dhe një zhvillim rus - mosml (dialekti "Moskë" ML ).

Afërsia me formalizimin matematik dhe orientimi fillestar funksional çoi në avantazhet e mëposhtme të qasjes funksionale:

1. thjeshtësia e testimit dhe verifikimit të kodit të programit bazuar në mundësinë e ndërtimit të një prove rigoroze matematikore të korrektësisë së programeve;

2. unifikimi i paraqitjes së programit dhe të dhënave (të dhënat mund të inkapsulohen në program si argumente të funksioneve, vlerësimi ose llogaritja e vlerës së funksionit mund të kryhet sipas nevojës);

3. shtypja e sigurt: operacionet e pavlefshme të të dhënave janë të përjashtuara;

4. Shtypja dinamike: është e mundur të zbulohen gabimet e shtypjes në kohën e ekzekutimit (mungesa e kësaj vetie në gjuhët e hershme të programimit funksional mund të çojë në një tejmbushje të RAM-it të kompjuterit);

5. Pavarësia e zbatimit të softuerit nga përfaqësimi i të dhënave të makinës dhe arkitektura e sistemit të programit (programuesi fokusohet në detajet e zbatimit, dhe jo në veçoritë e paraqitjes së të dhënave të makinës).

Vini re se realizimi i avantazheve që ofrojnë gjuhët funksionale të programimit varet shumë nga zgjedhja e platformës softuerike dhe harduerit.

Në rastin e zgjedhjes së teknologjisë .NET si një platformë softuerike, praktikisht pavarësisht nga zbatimi i harduerit, programuesi ose menaxheri i projektit të softuerit përfiton gjithashtu përparësitë e mëposhtme:

1. Integrimi i gjuhëve të ndryshme të programimit funksional (duke maksimizuar avantazhet e secilës prej gjuhëve, në veçanti, Skema ofron një mekanizëm të përputhjes së modelit, dhe SML siguron aftësinë për të llogaritur sipas nevojës);

2. Integrimi i qasjeve të ndryshme të programimit bazuar në Infrastrukturën e Përbashkët Gjuhësore, ose CLI (në veçanti, është e mundur të përdoret C # për të ofruar avantazhet e një qasjeje të orientuar drejt objektit dhe SML - funksionale, si në këtë kurs);

3. Sistemi i përbashkët i unifikuar i shtypjes Common Type System, CTS (menaxhimi uniform dhe i sigurt i llojeve të të dhënave në program);

4. një sistem shumëfazor, fleksibël për të siguruar sigurinë e kodit të programit (në veçanti, bazuar në mekanizmin e montimit).

Karakteristikat kryesore të gjuhëve funksionale të programimit që i dallojnë ato nga gjuhët imperative dhe gjuhët logjike të programimit janë transparenca me referencë dhe determinizëm. Në gjuhët funksionale, ka një shpërndarje të konsiderueshme në parametra të tillë si shtypja, rregullat e llogaritjes. Në shumë gjuhë, rendi i vlerësimit është i përcaktuar rreptësisht. Por ndonjëherë gjuhët strikte ofrojnë mbështetje për disa elementë të dobishëm të natyrshëm në gjuhët jo strikte, siç janë listat e pafundme (ML Standard ka një modul të veçantë për të mbështetur llogaritjen dembel). Në të kundërt, gjuhët e dobëta lejojnë llogaritjen e energjisë në disa raste.

Për shembull, Miranda ka semantikë dembele, por ju lejon të specifikoni konstruktorë të fortë duke etiketuar argumentet e konstruktorit në një mënyrë të caktuar.

Shumë gjuhë programimi funksionale moderne janë gjuhë të shtypura fort (shtypje e fortë). Shkrimi i fortë ofron më shumë siguri. Shumë gabime mund të rregullohen në kohën e përpilimit, kështu që faza e korrigjimit dhe koha e përgjithshme e zhvillimit zvogëlohen. Shtypja e fortë i lejon përpiluesit të gjenerojë kode më efikase dhe në këtë mënyrë të përshpejtojë ekzekutimin e programit. Së bashku me këtë, ekzistojnë gjuhë funksionale të shtypura në mënyrë dinamike. Lloji i të dhënave në gjuhë të tilla përcaktohet në kohën e ekzekutimit (Kapitulli 3). Ata nganjëherë quhen "pa tip". Përparësitë e tyre përfshijnë faktin se programet e shkruara në këto gjuhë kanë një përgjithësim më të madh. Disavantazhi mund të konsiderohet atribuimi i shumë gabimeve në fazën e ekzekutimit të programit dhe nevoja shoqëruese për të përdorur funksionet e kontrollit të tipit dhe reduktimi përkatës në përgjithësinë e programit. Gjuhët e shtypura priren të gjenerojnë kode më "të besueshme", ndërsa gjuhët e shtypura janë më "të përgjithshme".

Kriteri tjetër me të cilin mund të klasifikohen gjuhët funksionale të programimit mund të jetë prania e mekanizmave imperativ. Në të njëjtën kohë, është zakon të quhen gjuhë programimi funksionale pa mekanizma imperativë "të pastra", dhe ato me to - "të papastra". Në përmbledhjen e gjuhëve funksionale të programimit më poshtë, gjuhët e programimit do të referohen si "praktike" dhe "akademike". Me gjuhë "praktike" nënkuptojmë gjuhët që kanë një aplikacion komercial (ato janë përdorur për të zhvilluar aplikacione reale ose ishin sisteme programimi komerciale). Gjuhët e programimit akademik janë të njohura në qarqet kërkimore dhe në arsimin kompjuterik, por praktikisht nuk ka aplikacione komerciale të shkruara në gjuhë të tilla. Ato mbeten vetëm një mjet për kërkime teorike në fushën e informatikës dhe përdoren gjerësisht në procesin arsimor.

Lista e gjuhëve më të njohura të programimit funksional është dhënë më poshtë duke përdorur kriteret e mëposhtme: informacion i përgjithshëm; shtypja; lloji i llogaritjes; pastërti.

Lisp e zakonshme. Versioni Lisp, i cili që nga viti 1970 mund të konsiderohet standardi i gjuhës, falë mbështetjes nga Laboratori i Inteligjencës Artificiale MIT, është pa tip, energjik, me një grup të madh përfshirjesh imperative që lejojnë caktimin dhe shkatërrimin e strukturave. Praktike. Mjafton të thuhet se redaktori i grafikës vektoriale AutoCAD është shkruar në Lisp.

Skema. Një dialekt Lisp i destinuar për kërkimin e shkencave kompjuterike dhe mësimdhënien e programimit funksional. Për shkak të mungesës së përfshirjeve imperative, gjuha është shumë më e vogël se Common Lisp. Kthehet në gjuhën e zhvilluar nga J. McCarthy në 1962. Akademike, pa tipare, energjike, e pastër.

Refal. Një familje gjuhësh e zhvilluar nga V.F. Turchin. Anëtari më i vjetër i kësaj familje është realizuar për herë të parë në vitin 1968 në Rusi. Përdoret ende gjerësisht në qarqet akademike. Përmban elemente të programimit logjik (përputhja e modelit). Prandaj, Refal propozohet si një gjuhë vetë-studimi në këtë tutorial.

Miranda. I shtypur fuqishëm, mbështet llojet e të dhënave të përdoruesit dhe polimorfizmin. Zhvilluar nga Turner bazuar në gjuhët e mëparshme SALS dhe KRC. Ka semantikë dembele. Nuk ka përfshirje imperative.

Haskell. Zhvillimi i gjuhës ndodhi në fund të shekullit të kaluar. I njohur gjerësisht në rrethet akademike. Në disa universitete perëndimore përdoret si gjuha kryesore për të studiuar studentët. Një nga gjuhët funksionale më të fuqishme. Gjuhë dembel. Një gjuhë thjesht funksionale. E shtypur. Haskell është një mjet i shkëlqyeshëm për të mësuar dhe eksperimentuar me lloje komplekse të të dhënave funksionale. Programet e shkruara në Haskell kanë një madhësi të konsiderueshme të kodit të objektit dhe shpejtësi të ngadaltë të ekzekutimit.

I pastër. Një dialekt i Haskellit i përshtatur nevojave të programimit praktik. Ashtu si Haskell, është një gjuhë dembele, thjesht funksionale që përmban klasa tipi. Por Clean gjithashtu përmban veçori interesante që nuk kanë ekuivalent Haskell. Për shembull, tiparet imperative në Clean bazohen në lloje unike, të cilat janë të frymëzuara nga logjika lineare. Clean përmban mekanizma që mund të përmirësojnë ndjeshëm efikasitetin e programeve. Midis këtyre mekanizmave, llogaritja e shtyrë është e shtypur qartë. Implementimi Clean është një produkt komercial, por një version falas është i disponueshëm për qëllime kërkimore dhe edukative.

ML (Gjuha Meta). Zhvilluar nga një grup programuesish të udhëhequr nga Robert Milier në mesin e viteve '70. në Edinburg (Edinburgh Logic for Computable Functions). Ideja prapa gjuhës ishte krijimi i një mekanizmi për ndërtimin e provave formale në një sistem logjik për funksionet e llogaritshme. Në vitin 1983 gjuha u rishikua me koncepte të tilla si module. U bë i njohur si ML standarde. ML është një gjuhë e shtypur fort me shtypje statike dhe ekzekutim aplikativ të programit. Ajo ka fituar shumë popullaritet në qarqet kërkimore dhe në fushën e edukimit kompjuterik.

Vargu i kundërt (arg i vargut) (nëse (arg.gjatësia == 0) (arg kthimi;) else (kthimi i kundërt (arg.nënstring (1, arg.gjatësia)) + arg.nënstring (0, 1);))
Ky funksion është mjaft i ngadalshëm sepse ri-thirre veten. Një rrjedhje e kujtesës është e mundur këtu, pasi objektet e përkohshme krijohen shumë herë. Por ky është një stil funksional. Mund t'ju tregohet e çuditshme se si njerëzit mund të programojnë në atë mënyrë. Epo, sapo isha gati t'ju tregoja.

Përfitimet e programimit funksional

Ju ndoshta mendoni se nuk mund të argumentoj për të justifikuar funksionin monstruoz të mësipërm. Kur fillova të mësoja programimin funksional, mendova gjithashtu. Isha gabim. Ka argumente shumë të mira për këtë stil. Disa prej tyre janë subjektive. Për shembull, programuesit pretendojnë se programet funksionale janë më të lehta për t'u kuptuar. Nuk do të jap argumente të tilla, sepse të gjithë e dinë që lehtësia e të kuptuarit është një gjë shumë subjektive. Për fatin tim, ka ende shumë argumente objektive.

Testimi i njësisë

Meqenëse çdo simbol në FP është i pandryshueshëm, funksionet nuk kanë efekte anësore. Ju nuk mund të ndryshoni vlerat e variablave, për më tepër, një funksion nuk mund të ndryshojë një vlerë jashtë fushëveprimit të tij, dhe në këtë mënyrë të ndikojë në funksione të tjera (siç mund të ndodhë me fushat e klasës ose variablat globale). Kjo do të thotë që rezultati i vetëm i një funksioni është vlera e kthimit. Dhe e vetmja gjë që mund të ndikojë në vlerën e kthimit janë argumentet e kaluara në funksion.

Këtu është, ëndrra blu e testuesve të njësive. Ju mund të testoni çdo funksion në programin tuaj duke përdorur vetëm argumentet që dëshironi. Nuk ka nevojë të thirrni funksionet në rendin e duhur ose të rikrijoni gjendjen e duhur të jashtme. E tëra çfarë ju duhet të bëni është të kaloni argumente që përputhen me rastet e skajeve. Nëse të gjitha funksionet në programin tuaj kalojnë testet e njësisë, atëherë mund të jeni shumë më të sigurt në cilësinë e softuerit tuaj sesa në rastin e gjuhëve të programimit imperativ. Në Java ose C ++, kontrollimi i vlerës së kthimit nuk është i mjaftueshëm - funksioni mund të ndryshojë gjendjen e jashtme, e cila gjithashtu duhet të kontrollohet. Nuk ka një problem të tillë në FP.

Korrigjimi

Nëse një program funksional nuk sillet siç prisni, atëherë korrigjimi i gabimeve është një fllad. Ju gjithmonë mund ta riprodhoni problemin, sepse gabimi në funksion nuk varet nga kodi i jashtëm që është ekzekutuar më parë. Në një program imperativ, gabimi shfaqet vetëm për një kohë. Do t'ju duhet të kaloni nëpër një sërë hapash që nuk lidhen me gabimet, sepse një funksion varet nga gjendja e jashtme dhe efektet anësore të funksioneve të tjera. Në FP, situata është shumë më e thjeshtë - nëse vlera e kthimit është e gabuar, atëherë do të jetë gjithmonë e gabuar, pavarësisht se cilat pjesë të kodit janë ekzekutuar më parë.

Pasi të riprodhoni gabimin, gjetja e burimit është e parëndësishme. Madje është bukur. Sapo të ndaloni ekzekutimin e programit, do të keni para vetes të gjithë grupin e thirrjeve. Ju mund të shikoni argumentet e thirrjes së secilit funksion, ashtu si në një gjuhë imperative. Me ndryshimin se kjo nuk mjafton në një program imperativ, sepse funksionet varen nga vlerat e fushave, variablave globale dhe gjendjeve të klasave të tjera. Një funksion në FP varet vetëm nga argumentet e tij dhe ky informacion është pikërisht para syve tuaj! Për më tepër, në një program imperativ, kontrollimi i vlerës së kthyer nuk është i mjaftueshëm për të treguar nëse një pjesë e kodit po sillet siç duhet. Do t'ju duhet të gjuani dhjetëra objekte jashtë funksionit për t'u siguruar që gjithçka po funksionon siç duhet. Në programimin funksional, gjithçka që duhet të bëni është të shikoni vlerën e kthimit!

Ndërsa ecni nëpër pirg, vini re që argumentet kalohen dhe vlerat e kthimit. Sapo vlera e kthimit të devijojë nga norma, ju futeni më thellë në funksion dhe vazhdoni përpara. Kjo përsëritet disa herë derisa të gjeni burimin e gabimit!

Multithreading

Programi funksional është menjëherë gati për paralelizim pa asnjë ndryshim. Nuk duhet të shqetësoheni për ngërçet apo kushtet e garës sepse nuk keni nevojë për bravë! Asnjë pjesë e vetme e të dhënave në një program funksional nuk ndryshohet dy herë nga i njëjti rrymë ose nga të ndryshëm. Kjo do të thotë që ju mund të shtoni lehtësisht tema në programin tuaj pa menduar as për problemet e natyrshme në gjuhët imperative.

Nëse është kështu, atëherë pse gjuhët funksionale të programimit përdoren kaq rrallë në aplikacione me shumë fije? Më shpesh sesa mendoni, në fakt. Ericsson ka zhvilluar një gjuhë funksionale të quajtur Erlang për përdorim në çelsat e telekomunikacionit tolerantë ndaj gabimeve dhe të shkallëzuara. Shumë njerëz vunë re avantazhet e Erlang dhe filluan ta përdorin atë. Ne po flasim për sistemet e telekomunikacionit dhe kontrollit të trafikut që nuk përshkallëzohen aq lehtë sa sistemet tipike të zhvilluara në Wall Street. Në përgjithësi, sistemet e shkruara në Erlang nuk janë aq të shkallëzueshme dhe të besueshme sa sistemet Java. Sistemet Erlang janë super të besueshme.

Historia e multithreading nuk mbaron këtu. Nëse jeni duke shkruar një aplikacion në thelb me një fije të vetme, përpiluesi mund të optimizojë një program funksional për të përdorur shumë CPU. Le të hedhim një vështrim në pjesën tjetër të kodit.


Një përpilues gjuhësor funksional mund të analizojë kodin, të klasifikojë funksionet që krijojnë vargjet s1 dhe s2 si funksione që konsumojnë kohë dhe t'i ekzekutojë ato paralelisht. Kjo nuk mund të bëhet në një gjuhë imperative, sepse çdo funksion mund të ndryshojë gjendjen e jashtme dhe kodi menjëherë pas thirrjes mund të varet nga ai. Në FP, analiza automatike e funksioneve dhe kërkimi i kandidatëve të përshtatshëm për paralelizim është po aq i parëndësishëm sa një inline automatik! Në këtë kuptim, stili funksional i programimit plotëson kërkesat e së ardhmes. Zhvilluesit e harduerit nuk mund ta bëjnë më CPU-në të funksionojë më shpejt. Në vend të kësaj, ata po rrisin numrin e bërthamave dhe po pretendojnë një rritje katërfish në shpejtësinë e llogaritjes me shumë fije. Sigurisht, ata harrojnë të thonë shumë mirë me kohë se procesori juaj i ri do të tregojë një rritje vetëm në programet e zhvilluara duke pasur parasysh paralelizmin. Ka shumë pak prej tyre në mesin e softuerëve imperativë. Por 100% e programeve funksionale janë gati për multithreading jashtë kutisë.

Shpërndarja e nxehtë

Në kohët e vjetra, duhej të rindizje kompjuterin për të instaluar përditësimet e Windows. Shume here. Pas instalimit të një versioni të ri të luajtësit të mediave. Ka pasur ndryshime të rëndësishme në Windows XP, por situata është ende larg idealit (sot kam ekzekutuar Windows Update në punë dhe tani kujtesa e bezdisshme nuk do të më lërë të qetë derisa të rindizem). Në sistemet Unix, modeli i përmirësimit ishte më i mirë. Për të instaluar përditësime, duhej të ndaloje disa komponentë, por jo të gjithë OS. Megjithëse situata duket më e mirë, ajo ende nuk është e pranueshme për një klasë të madhe aplikacionesh serverësh. Sistemet e telekomunikacionit duhet të ndizen 100% të rasteve, sepse nëse, për shkak të përditësimit, një person nuk mund të telefonojë një ambulancë, atëherë mund të humbasin jetë. Firmat me Wall Streets gjithashtu nuk duan të mbyllin serverët gjatë fundjavës për të instaluar përditësime.

Në mënyrë ideale, ju duhet të përditësoni të gjitha pjesët e nevojshme të kodit pa e ndalur sistemin në parim. Në botën imperative, kjo është e pamundur [trans. në Smalltalk është shumë e mundur]. Imagjinoni të shkarkoni një klasë Java në fluturim dhe të rifreskoni një version të ri. Nëse do ta bënim këtë, atëherë të gjitha instancat e klasës do të bëheshin jofunksionale, sepse gjendja që ata mbanin do të humbiste. Do të na duhej të shkruanim kod të ndërlikuar për kontrollin e versionit. Ju do të duhet të serializoni të gjitha instancat e krijuara të klasës, pastaj t'i shkatërroni ato, të krijoni shembuj të klasës së re, të përpiqeni të ngarkoni të dhënat e serializuara me shpresën se migrimi do të shkojë mirë dhe instancat e reja do të jenë të vlefshme. Dhe përveç kësaj, kodi i migrimit duhet të shkruhet manualisht çdo herë. Dhe kodi i migrimit duhet gjithashtu të ruajë lidhjet midis objekteve. Në teori, gjithçka është në rregull, por në praktikë nuk do të funksionojë kurrë.

Në një program funksional, të gjitha gjendjet ruhen në stek si argumente funksioni. Kjo e bën vendosjen e nxehtë shumë më të lehtë! Në thelb, gjithçka që duhet të bëni është të llogarisni ndryshimin midis kodit në serverin e prodhimit dhe versionit të ri dhe të instaloni ndryshimet në kod. Pjesa tjetër do të bëhet automatikisht nga mjetet gjuhësore! Nëse mendoni se ky është fantashkencë, atëherë mendoni dy herë. Inxhinierët e Erlang kanë përditësuar sistemet e tyre për vite me rradhë pa i ndaluar ato.

Llogaritja dhe optimizimi i provave (provat dhe optimizimet me ndihmën e makinës)

Një veçori tjetër interesante e gjuhëve të programimit funksional është se ato mund të studiohen matematikisht. Meqenëse një gjuhë funksionale është një zbatim i një sistemi formal, të gjitha veprimet matematikore të përdorura në letër mund të aplikohen në programet funksionale. Përpiluesi, për shembull, mund të konvertojë një pjesë të kodit në një pjesë ekuivalente, por më efikase, duke vërtetuar matematikisht ekuivalencën e tyre. Bazat e të dhënave relacionale i kanë bërë këto optimizime prej vitesh. Asgjë nuk ju pengon të përdorni teknika të ngjashme në programet e zakonshme.

Për më tepër, mund të përdorni mjete matematikore për të vërtetuar korrektësinë e seksioneve të programeve tuaja. Nëse dëshironi, mund të shkruani mjete që analizojnë kodin dhe krijojnë automatikisht teste Unit për rastet e skajeve! Ky funksionalitet është i paçmuar për sistemet e forta shkëmbore. Kur zhvillohen sisteme për monitorimin e stimuluesve kardiak ose menaxhimin e trafikut ajror, këto mjete janë thelbësore. Nëse zhvillimet tuaja nuk janë në fushën e aplikacioneve kritike, atëherë mjetet e verifikimit automatik do t'ju japin akoma një avantazh të madh ndaj konkurrentëve tuaj.

Funksionet e rendit më të lartë

Mos harroni, kur fola për përfitimet e FP, vura re se "çdo gjë duket bukur, por është e kotë nëse më duhet të shkruaj në një gjuhë të ngathët në të cilën gjithçka është përfundimtare". Ky ishte një iluzion. Përdorimi i fundit në të gjithë vendin duket i ngathët vetëm në gjuhë programimi imperative si Java. Gjuhët e programimit funksional operojnë me lloje të tjera abstraksionesh, të tilla që ju harroni se dikur keni dashur të ndryshoni variablat. Një mjet i tillë janë funksionet e rendit më të lartë.

Në FP, një funksion nuk është i njëjtë me një funksion në Java ose C. Është një superset - ato mund të jenë të njëjta me funksionet Java dhe madje edhe më shumë. Le të themi se kemi një funksion në C:

Int add (int i, int j) (kthim i + j;)
Në FP, ky nuk është i njëjtë me një funksion të rregullt C. Le të zgjerojmë përpiluesin tonë Java për të mbështetur këtë shënim. Përpiluesi duhet ta kthejë deklaratën e funksionit në kodin e mëposhtëm Java (mos harroni se fundi i nënkuptuar është kudo):

Klasa add_function_t (int add (int i, int j) (kthim i + j;)) add_function_t add = new add_function_t ();
Simboli i shtimit nuk është në të vërtetë një funksion. Është një klasë e vogël me një metodë. Tani mund të kalojmë add si argument për funksionet e tjera. Mund ta shkruajmë në një simbol tjetër. Ne mund të krijojmë shembuj të add_function_t në kohën e ekzekutimit dhe ato do të mblidhen mbeturina nëse nuk nevojiten më. Funksionet bëhen objekte bazë si numrat dhe vargjet. Funksionet që veprojnë në funksione (marrini ato si argumente) quhen funksione të rendit më të lartë. Mos lejoni që kjo t'ju trembë. Koncepti i funksioneve të rendit më të lartë është pothuajse i njëjtë me konceptin e klasave Java që veprojnë mbi njëra-tjetrën (mund t'i kalojmë klasat në klasa të tjera). Mund t'i quajmë "klasa të rendit të lartë", por askush nuk shqetësohet me këtë, sepse Java nuk ka një komunitet të rreptë akademik pas saj.

Si dhe kur duhet të përdorni funksione të rendit më të lartë? Më vjen mirë që pyete. Ju shkruani programin tuaj si një pjesë të madhe, monolit të kodit pa u shqetësuar për hierarkinë e klasës. Nëse shihni se një pjesë e kodit përsëritet në vende të ndryshme, ju e zhvendosni atë në një funksion të veçantë (për fat të mirë, shkollat ​​ende mësojnë se si ta bëni këtë). Nëse vëreni se një pjesë e logjikës në funksionin tuaj duhet të sillet ndryshe në disa situata, atëherë krijoni një funksion të rendit më të lartë. Të hutuar? Këtu është një shembull i botës reale nga puna ime.

Supozoni se kemi një pjesë të kodit Java që merr një mesazh, e transformon atë në mënyra të ndryshme dhe e dërgon në një server tjetër.

Void handleMessage (mesazhi i mesazhit) (// ... msg.setClientCode ("ABCD_123"); // ... dërgo Mesazh (msg);) // ...)
Tani imagjinoni që sistemi ka ndryshuar dhe tani ju duhet të shpërndani mesazhe midis dy serverëve në vend të njërit. Gjithçka mbetet e pandryshuar, përveç kodit të klientit - serveri i dytë dëshiron ta marrë këtë kod në një format tjetër. Si ta përballojmë këtë situatë? Ne mund të kontrollojmë se ku duhet të shkojë mesazhi dhe të vendosim kodin e saktë të klientit bazuar në atë. Për shembull si kjo:

Klasa Message Handler (void handleMessage (message msg) (// ... if (msg.getDestination (). Është i barabartë ("server1") (msg.setClientCode ("ABCD_123");) tjetër (msg.setClientCode ("123_ABC") ;) // ... dërgo Mesazh (msg);) // ...)
Por kjo qasje nuk ka shkallë të mirë. Ndërsa shtohen serverë të rinj, funksioni do të rritet në mënyrë lineare dhe bërja e ndryshimeve do të bëhet një makth. Qasja e orientuar nga objekti është që të nënklasohet superklasa e përgjithshme MessageHandler dhe të nënklasohet logjika e përkufizimit të kodit të klientit:

Klasa abstrakte MessageHandler (void handleMessage (message msg) (// ... msg.setClientCode (getClientCode ()); // ... sendMessage (msg);) string abstrakt getClientCode (); // ...) klasa MessageHandlerOne zgjeron MessageHandler (String getClientCode () (kthehet "ABCD_123";)) klasa MessageHandlerTwo zgjeron MessageHandler (String getClientCode () (kthim "123_ABCD";))
Tani, për çdo server, ne mund të krijojmë një shembull të klasës përkatëse. Shtimi i të rejave nga serveri bëhet më i përshtatshëm. Por për një ndryshim kaq të vogël, shumë tekst. Duhej të krijoheshin dy lloje të reja vetëm për të shtuar mbështetje për kode të ndryshme klienti! Tani le të bëjmë të njëjtën gjë në gjuhën tonë me mbështetjen për funksionet e rendit më të lartë:

Klasa MessageHandler (void handleMessage (message message, Function getClientCode) (// ... Message msg1 = msg.setClientCode (getClientCode ()); // ... sendMessage (msg1);) // ...) String getClientCodeOne ( ) (kthejeni "ABCD_123";) String getClientCodeTwo () (kthejeni "123_ABCD";) mbajtës i mesazheve = mbajtës i ri i mesazheve (); handler.handleMessage (someMsg, getClientCodeOne);
Ne nuk krijuam lloje të reja apo ndërlikuam hierarkinë e klasave. Sapo e kaluam funksionin si parametër. Ne kemi arritur të njëjtin efekt si në homologun e orientuar nga objekti, me vetëm disa avantazhe. Ne nuk e lidhim veten me asnjë hierarki klase: ne mund të kalojmë çdo funksion tjetër në kohën e ekzekutimit dhe t'i ndryshojmë ato në çdo kohë, duke ruajtur një nivel të lartë modulariteti me më pak kod. Në thelb, përpiluesi krijoi ngjitësin e orientuar nga objekti për ne! Në të njëjtën kohë, të gjitha avantazhet e tjera të FP ruhen. Sigurisht, abstraksionet e ofruara nga gjuhët funksionale nuk mbarojnë këtu. Funksionet e rendit më të lartë janë vetëm fillimi

Currying

Shumica e njerëzve që takoj kanë lexuar Modelet e Dizajnit të Gang of Four. Çdo programues që respekton veten do të thotë se libri nuk është i lidhur me ndonjë gjuhë programimi të veçantë dhe modelet janë të zbatueshme për zhvillimin e softuerit në përgjithësi. Kjo është një deklaratë fisnike. Por fatkeqësisht është larg së vërtetës.

Gjuhët funksionale janë tepër shprehëse. Në një gjuhë funksionale, nuk keni nevojë për modele dizajni sepse gjuha është aq e nivelit të lartë sa mund të filloni lehtësisht të programoni në koncepte që përjashtojnë të gjitha modelet e njohura të programimit. Një model i tillë është përshtatësi (si ndryshon nga Fasada? Duket sikur dikush duhej të vuloste më shumë faqe për të përmbushur kushtet e kontratës). Ky model është i panevojshëm nëse gjuha ka mbështetje për currying.

Modeli i përshtatësit aplikohet më shpesh në njësinë "standarde" të abstraksionit në një klasë Java. Në gjuhët funksionale, modeli zbatohet për funksionet. Modeli merr një ndërfaqe dhe e transformon atë në një ndërfaqe tjetër, sipas kërkesave të caktuara. Këtu është një shembull i një modeli përshtatës:

Int pow (int i, int j); katror int (int i) (pow kthimi (i, 2);)
Ky kod përshtat ndërfaqen e një funksioni që ngre një numër në një fuqi arbitrare me ndërfaqen e një funksioni që vendos në katror një numër. Në qarqet akademike, kjo teknikë më e thjeshtë quhet currying (sipas logjikës Haskell Curry, i cili kreu një sërë trukesh matematikore për t'i zyrtarizuar të gjitha). Meqenëse funksionet përdoren zakonisht si argumente në FP, currying përdoret shumë shpesh për të sjellë funksionet në një ndërfaqe që nevojitet në një vend ose në një tjetër. Meqenëse ndërfaqja e një funksioni është argumentet e tij, currying përdoret për të reduktuar numrin e argumenteve (si në shembullin e mësipërm).

Ky mjet është ndërtuar në gjuhë funksionale. Nuk është e nevojshme të krijoni manualisht një funksion që mbështjell origjinalin. Një gjuhë funksionale do të bëjë gjithçka për ju. Si zakonisht, le ta zgjerojmë gjuhën tonë duke i shtuar edhe currying.

Sheshi = int pow (int i, 2);
Me këtë linjë, ne krijojmë automatikisht një funksion katror me një argument. Funksioni i ri do të thërrasë pow, duke zëvendësuar 2 për argumentin e dytë. Nga një këndvështrim Java, do të duket kështu:

Klasa katrori_funksioni_t (int katror (int i) (kthimi pow (i, 2);)) katrori_funksioni_t katror = katrori_funksioni_t i ri ();
Siç mund ta shihni, ne sapo shkruam një mbështjellës mbi funksionin origjinal. Në FP, currying është vetëm një mënyrë e thjeshtë dhe e përshtatshme për të krijuar mbështjellës. Ju përqendroheni në detyrë, dhe përpiluesi shkruan kodin e nevojshëm për ju! Është shumë e thjeshtë dhe ndodh sa herë që dëshironi të përdorni modelin e përshtatësit (mbështjellësit).

Vlerësim dembel

Vlerësimi dembel (ose dembel) është një teknikë interesante që bëhet e mundur sapo të kuptoni filozofinë funksionale. Ne kemi parë tashmë pjesën e mëposhtme të kodit kur folëm për multithreading:

Vargu s1 = disi i gjatëOperation1 (); Vargu s2 = disiOperacion i gjatë2 (); Vargu s3 = bashkoj (s1, s2);
Në gjuhët imperative të programimit, rendi i vlerësimit nuk ngre asnjë pyetje. Meqenëse çdo funksion mund të ndikojë ose të varet nga një gjendje e jashtme, është e nevojshme të vëzhgoni një renditje të qartë të thirrjeve: së pari, disiOperationLongOperation1, pastaj disi LongOperation2, dhe bashkohen në fund. Por jo gjithçka është kaq e thjeshtë në gjuhët funksionale.

Siç e pamë më herët, disiLongOperation1 dhe SomewhatLongOperation2 mund të nisen në të njëjtën kohë, sepse funksionet janë të garantuara që të mos preken dhe të mos varen nga gjendja globale. Por çfarë nëse nuk duam t'i ekzekutojmë ato në të njëjtën kohë, a duhet t'i thërrasim ato në mënyrë sekuenciale? Përgjigja është jo. Këto llogaritje duhet të kryhen vetëm nëse një funksion tjetër varet nga s1 dhe s2. Ne as nuk kemi nevojë t'i ekzekutojmë ato për sa kohë që na duhen brenda bashkimit. Nëse në vend të bashkimit zëvendësojmë një funksion që, në varësi të kushtit, përdor një nga dy argumentet, atëherë argumenti i dytë nuk duhet as të llogaritet! Haskell është një shembull i një gjuhe kompjuterike dembele. Haskell nuk garanton asnjë renditje të thirrjeve (fare!), Sepse Haskell ekzekuton kodin sipas nevojës.

Vlerësimi dembel ka disa avantazhe si dhe disa disavantazhe. Në pjesën tjetër, ne do të diskutojmë meritat dhe unë do të shpjegoj se si të përballemi me disavantazhet.

Optimizimi

Vlerësimi dembel ofron një potencial të jashtëzakonshëm për optimizim. Një përpilues dembel e trajton kodin tamam si një matematikan që studion shprehjet algjebrike - ai mund të zhbëjë disa gjëra, të anulojë ekzekutimin e disa seksioneve të kodit, të ndryshojë rendin e thirrjeve për efikasitet më të madh, madje të rregullojë kodin në atë mënyrë që të zvogëlojë numri i gabimeve, duke garantuar integritetin e programit. Ky është avantazhi më i madh kur përshkruani një program me primitivë të rreptë formal - kodi u bindet ligjeve matematikore dhe mund të studiohet me metoda matematikore.

Abstragimi i strukturave të kontrollit

Vlerësimi dembel siguron një nivel kaq të lartë abstraksioni, saqë gjërat e mahnitshme bëhen të mundshme. Për shembull, imagjinoni zbatimin e strukturës së mëposhtme të kontrollit:

Përveç nëse (stock.isEuropean ()) (sendToSEC (stock);)
Ne duam që funksioni sendToSEC të ekzekutohet vetëm nëse fondi (stoku) nuk është evropian. Si mund të zbatoni nëse? Pa vlerësim dembel, do të na duhej një sistem makro, por në gjuhë si Haskell, kjo nuk është e nevojshme. Ne mund të deklarojmë përveç nëse si funksion!

I pavlefshëm përveç rastit (kushti boolean, kodi i listës) ​​(nëse (! Kushti) kodi;)
Vini re se kodi nuk do të ekzekutohet nëse kushti == i vërtetë. Në gjuhët e forta, kjo sjellje nuk mund të përsëritet, pasi argumentet do të vlerësohen më parë nëse nuk thirret.

Struktura të pafundme të të dhënave

Gjuhët dembelë ju lejojnë të krijoni struktura të pafundme të dhënash, të cilat janë shumë më të vështira për t'u krijuar në gjuhë strikte [trans. - jo në Python]. Për shembull, imagjinoni një sekuencë Fibonacci. Natyrisht, ne nuk mund të llogarisim një listë të pafundme në kohë të fundme dhe ta ruajmë atë në memorie. Në gjuhë të forta si Java, ne thjesht do të shkruanim një funksion që kthen një anëtar arbitrar të një sekuence. Në gjuhë si Haskell, ne mund të abstraktojmë dhe thjesht të deklarojmë një listë të pafundme numrash Fibonacci. Meqenëse gjuha është dembel, do të llogariten vetëm pjesët e nevojshme të listës që përdoren aktualisht në program. Kjo ju lejon të abstraktoni nga një numër i madh problemesh dhe t'i shikoni ato nga një nivel më i lartë (për shembull, mund të përdorni funksionet e përpunimit të listave në sekuenca të pafundme).

Të metat

Sigurisht, djathi falas është vetëm në një kurth miu. Vlerësimi dembel ka një sërë disavantazhesh. Këto janë kryesisht disavantazhe nga dembelizmi. Në realitet, shpesh nevojitet një renditje e drejtpërdrejtë e llogaritjes. Merrni, për shembull, kodin e mëposhtëm:


Në një gjuhë dembel, askush nuk garanton që rreshti i parë do të ekzekutohet para të dytës! Kjo do të thotë që ne nuk mund të bëjmë I / O, nuk mund të përdorim normalisht funksionet vendase (në fund të fundit, ato duhet të thirren në një rend të caktuar për të marrë parasysh efektet e tyre anësore), dhe ne nuk mund të ndërveprojmë me botën e jashtme! Nëse prezantojmë një mekanizëm për të thjeshtuar ekzekutimin e kodit, atëherë do të humbasim avantazhin e ashpërsisë matematikore të kodit (dhe më pas do të humbasim të gjitha të mirat e programimit funksional). Për fat të mirë, ende nuk ka humbur gjithçka. Matematikanët filluan të punojnë dhe dolën me disa truke për t'u siguruar që udhëzimet ishin në rendin e duhur pa humbur frymën e tyre funksionale. Ne morëm më të mirën e të dy botëve! Teknika të tilla përfshijnë vazhdimin, monadat dhe shtypjen e veçantisë. Në këtë artikull do të punojmë me vazhdime, dhe do të shtyjmë monadat dhe shtypjen e paqartë për herën tjetër. Është interesante se vazhdimi është një gjë shumë e dobishme, e cila përdoret jo vetëm për të specifikuar një renditje të rreptë të llogaritjeve. Ne gjithashtu do të flasim për këtë.

Vazhdimet

Vazhdimet në programim luajnë të njëjtin rol si Kodi i Da Vinçit në historinë njerëzore: zbulimi mahnitës i misterit më të madh të njerëzimit. Epo, mbase jo fare, por ato padyshim që heqin perdet, pasi dikur mësuat të merrnit rrënjën e -1.

Kur shikuam funksionet, mësuam vetëm gjysmën e së vërtetës, sepse dolëm nga supozimi se një funksion i kthen një vlerë funksionit thirrës. Në këtë kuptim, vazhdimi është një përgjithësim i funksioneve. Funksioni nuk duhet të kthejë kontrollin në vendin nga ku është thirrur, por mund të kthehet në çdo vend të programit. Continue është një parametër që mund t'ia kalojmë një funksioni për të treguar pikën e kthimit. Tingëllon shumë më e frikshme se sa është në të vërtetë. Le të hedhim një vështrim në kodin e mëposhtëm:

Int i = shtoni (5, 10); int j = katror (i);
Funksioni add kthen numrin 15, i cili shkruhet në i, në pikën ku u thirr funksioni. Vlera e i përdoret më pas në thirrjen në katror. Vini re se përpiluesi dembel nuk mund të ndryshojë rendin e vlerësimit, sepse rreshti i dytë varet nga rezultati i të parit. Ne mund ta rishkruajmë këtë kod duke përdorur stilin e kalimit të vazhdueshëm (CPS) kur add kthen një vlerë në funksionin katror.

Int j = shtoni (5, 10, katror);
Në këtë rast, add merr një argument shtesë - një funksion që do të thirret pasi shtimi të përfundojë duke punuar. Në të dy shembujt, j do të jetë 225.

Kjo është teknika e parë që ju lejon të specifikoni rendin e ekzekutimit të dy shprehjeve. Le të kthehemi te shembulli ynë I/O.

System.out.println ("Ju lutemi shkruani emrin tuaj:"); System.in.readLine ();
Këto dy rreshta janë të pavarura nga njëra-tjetra, dhe përpiluesi është i lirë të ndryshojë rendin e tyre sipas dëshirës. Por nëse e rishkruajmë atë në CPS, atëherë duke e bërë këtë shtojmë varësinë e kërkuar dhe përpiluesi do të duhet të kryejë llogaritjet njëra pas tjetrës!

System.out.println ("Ju lutemi shkruani emrin tuaj:", System.in.readLine);
Në një rast të tillë, println do të thërriste readLine me rezultatin e tij dhe do të kthente rezultatin readLine në fund. Në këtë formë, mund të jemi të sigurt se këto funksione do të thirren me radhë dhe se readLine do të thirret fare (në fund të fundit, përpiluesi pret të marrë rezultatin e operacionit të fundit). Në rastin e Java, println kthehet në void. Por nëse do të kthehej ndonjë vlerë abstrakte (e cila mund të shërbejë si argument për readLine), atëherë kjo do të zgjidhte problemin tonë! Sigurisht, rreshtimi i zinxhirëve të tillë funksionesh dëmton shumë lexueshmërinë e kodit, por ju mund ta luftoni këtë. Ne mund të shtojmë të mira sintaksore në gjuhën tonë që do të na lejojnë të shkruajmë shprehje si zakonisht, dhe përpiluesi do t'i lidh automatikisht llogaritjet. Tani ne mund të kryejmë llogaritjet në çdo mënyrë pa humbur avantazhet e FP (përfshirë aftësinë për të studiuar programin duke përdorur metoda matematikore)! Nëse kjo ju ngatërron, mbani mend se funksionet janë vetëm shembuj të një klase me një anëtar të vetëm. Rishkruajeni shembullin tonë në mënyrë që println dhe readLine të jenë të dyja shembuj të klasës në mënyrë që ta kuptoni më mirë.

Por përfitimet e vazhdimeve nuk mbarojnë këtu. Mund të shkruajmë një program të tërë duke përdorur CPS, në mënyrë që çdo funksion të thirret me një parametër shtesë, një vazhdim, në të cilin kalohet rezultati. Në parim, çdo program mund të përkthehet në CPS, nëse mendoni për secilin funksion si një rast të veçantë vazhdimësie. Ky konvertim mund të bëhet automatikisht (në fakt, shumë përpilues e bëjnë këtë).

Sapo e konvertojmë programin në formën CPS, bëhet e qartë se çdo instruksion ka një vazhdim, funksionin në të cilin do të kalohet rezultati, i cili në një program normal do të ishte një pikë thirrjeje. Le të marrim ndonjë udhëzim nga shembulli i fundit, për shembull add (5,10). Në një program të shkruar në formë CPS, është e qartë se çfarë do të jetë një vazhdim - ky është një funksion që shtimi do të thërrasë në fund të punës. Por cili do të jetë vazhdimi i programit jo-CPS? Sigurisht, ne mund ta konvertojmë programin në CPS, por a është e nevojshme?

Rezulton se kjo nuk është e nevojshme. Shikoni nga afër konvertimin tonë të CPS. Nëse filloni të shkruani një përpilues për të, do të zbuloni se versioni CPS nuk ka nevojë për një pirg! Funksionet nuk kthejnë kurrë asgjë, në kuptimin tradicional të fjalës "kthim", ata thjesht thërrasin një funksion tjetër, duke zëvendësuar rezultatin e llogaritjeve. Nuk ka nevojë të shtyni (shtyni) argumentet në pirg përpara çdo telefonate, dhe pastaj t'i ktheni ato. Ne thjesht mund t'i ruajmë argumentet në një pjesë fikse të memories dhe të përdorim kërcimin në vend të thirrjes normale. Nuk kemi nevojë të ruajmë argumentet origjinale, sepse nuk do të na duhen më kurrë, sepse funksionet nuk kthejnë asgjë!

Kështu, programet e stilit CPS nuk kanë nevojë për një pirg, por përmbajnë një argument shtesë, në formën e një funksioni që do të thirret. Programet jo të stilit CPS nuk kanë argument shtesë, por përdorin një pirg. Çfarë ruhet në pirg? Vetëm argumente dhe një tregues për vendndodhjen e memories ku funksioni duhet të kthehet. Epo, e menduat? Rafte ruan informacione rreth vazhdimeve! Një tregues për një pikë kthimi në pirg është i njëjtë me një funksion që duhet thirrur në programet CPS! Për të zbuluar se cila shtrirje është add (5,10), mjafton të marrësh pikën e kthimit nga steka.

Nuk ishte e vështirë. Një vazhdim dhe një tregues për një pikë kthimi janë në të vërtetë e njëjta gjë, vetëm vazhdimi është specifikuar në mënyrë eksplicite, dhe për këtë arsye mund të ndryshojë nga vendi ku është thirrur funksioni. Nëse mbani mend se një vazhdim është një funksion dhe një funksion në gjuhën tonë përpilohet në një shembull të një klase, atëherë do të kuptoni se një tregues në një pikë kthimi në pirg dhe një tregues në një vazhdim janë në fakt e njëjta gjë. , sepse funksioni ynë (si shembull i një klase ) është vetëm një tregues. Kjo do të thotë që në çdo kohë në programin tuaj, ju mund të kërkoni vazhdimin aktual (në fakt, informacion nga stack).

Mirë, tani kemi një ide të mirë se çfarë është vazhdimi aktual. Çfarë do të thotë? Nëse marrim vazhdimin aktual dhe e ruajmë diku, në këtë mënyrë ruajmë gjendjen aktuale të programit - e ngrijmë atë. Kjo është e ngjashme me letargjinë e OS. Objekti i vazhdimit ruan informacionin e nevojshëm për të rifilluar ekzekutimin e programit nga pika ku u kërkua objekti i vazhdimit. Sistemi operativ e bën këtë me programet tuaja gjatë gjithë kohës kur ndërron kontekstin midis temave. Dallimi i vetëm është se gjithçka është nën kontrollin e OS. Nëse kërkoni një objekt vazhdimësie (në Skemë, kjo bëhet duke thirrur funksionin call-with-current-continuation), atëherë do të merrni një objekt me vazhdimin aktual - stivën (ose në rastin e CPS - funksionin e thirrjen tjetër). Ju mund ta ruani këtë objekt në një variabël (ose edhe në disk). Nëse zgjidhni të "rifilloni" programin me këtë vazhdim, atëherë gjendja e programit tuaj "konvertohet" në gjendjen në të cilën ishte kur u mor objekti i vazhdimit. Kjo është njësoj si kalimi në një fije të pezulluar ose zgjimi i sistemit operativ pas letargjisë. Me përjashtim që mund ta bëni këtë shumë herë radhazi. Pasi OS zgjohet, informacioni i letargji shkatërrohet. Nëse kjo nuk është bërë, atëherë do të ishte e mundur të rivendosni gjendjen e OS nga e njëjta pikë. Është pothuajse si një udhëtim në kohë. Me vazhdime, ju mund ta përballoni atë!

Në cilat situata janë të dobishme vazhdimet? Zakonisht nëse po përpiqeni të imitoni gjendjen në sisteme pa atë në thelb. Vazhdimet përdoren mirë në aplikacionet në ueb (siç është kuadri Seaside Smalltalk). ASP.NET nga Microsoft bën përpjekje të mëdha për të ruajtur gjendjen midis kërkesave dhe për ta bërë jetën tuaj më të lehtë. Nëse C # mbështet vazhdimet, kompleksiteti i ASP.NET mund të zvogëlohet në gjysmë duke ruajtur vazhdimin dhe duke e rivendosur atë në kërkesën tjetër. Nga këndvështrimi i një programuesi në internet, nuk do të kishte pushim - programi do të vazhdonte në rreshtin tjetër! Vazhdimet janë një abstraksion tepër i dobishëm për zgjidhjen e disa problemeve. Me gjithnjë e më shumë klientë tradicionalë të trashë që lëvizin në ueb, rëndësia e vazhdimeve do të rritet vetëm me kalimin e kohës.

Përputhja e modelit

Përputhja e modelit nuk është një ide aq e re apo novatore. Në fakt, ka pak të bëjë me programimin funksional. Arsyeja e vetme që shpesh lidhet me FP është se për disa kohë gjuhët funksionale kanë përputhje modelesh, por ato imperative jo.

Le të fillojmë njohjen tonë me përputhjen e modelit me shembullin e mëposhtëm. Këtu është funksioni për llogaritjen e numrave Fibonacci në Java:

Int fib (int n) (nëse (n == 0) kthen 1; nëse (n == 1) kthe 1; ktheje fib (n - 2) + fib (n - 1);)
Dhe këtu është një shembull në një gjuhë të ngjashme me Java me mbështetje për përputhjen e modelit

Int fib (0) (kthimi 1;) int fib (1) (kthimi 1;) int fib (int n) (kthimi fib (n - 2) + fib (n - 1);)
Qfare eshte dallimi? Përpiluesi bën degëzimin për ne.

Vetëm mendoni, rëndësia është e madhe! Në të vërtetë, rëndësia nuk është e madhe. U vu re se një numër i madh funksionesh përmbajnë konstruksione komplekse ndërprerëse (kjo është pjesërisht e vërtetë për programet funksionale), dhe u vendos që të theksohej kjo pikë. Përkufizimi i funksionit ndahet në disa variante dhe modeli vendoset në vend të argumenteve të funksionit (kjo është e ngjashme me mbingarkimin e metodës). Kur thirret një funksion, përpiluesi krahason argumentet kundër të gjitha përkufizimeve në lëvizje dhe zgjedh atë më të përshtatshmen. Zakonisht zgjedhja bie në përkufizimin më të specializuar të funksionit. Për shembull int fib (int n) mund të quhet kur n është 1, por jo, sepse int fib (1) është një përkufizim më i specializuar.

Përputhja e modelit është zakonisht më komplekse se shembulli ynë. Për shembull, sistemi kompleks i përputhjes së modelit ju lejon të shkruani kodin e mëposhtëm:

Int f (int n< 10) { ... } int f(int n) { ... }
Kur mund të jetë e dobishme përputhja e modeleve? Lista e rasteve të tilla është çuditërisht e gjatë! Sa herë që përdorni konstruksione komplekse të mbivendosura, përputhja e modelit mund të funksionojë më mirë me më pak kod. Një shembull i mirë vjen në mendje me funksionin WndProc, i cili zbatohet në çdo program Win32 (edhe nëse është i fshehur nga programuesi pas një gardh të lartë abstraksionesh). Në mënyrë tipike, përputhja e modeleve mund të kontrollojë edhe përmbajtjen e koleksioneve. Për shembull, nëse i kaloni një grup një funksioni, atëherë mund të zgjidhni të gjitha vargjet në të cilat elementi i parë është 1 dhe elementi i tretë është më i madh se 3.

Një avantazh tjetër i përputhjes së modelit është se nëse bëni ndryshime, nuk keni nevojë të gërmoni në një funksion të madh. Thjesht duhet të shtoni (ose të ndryshoni) disa përkufizime funksioni. Kështu, ne heqim qafe një shtresë të tërë modelesh nga libri i famshëm i Bandës së Katërve. Sa më komplekse dhe më të degëzuara të jenë kushtet, aq më i dobishëm do të jetë përputhja e modelit. Pasi të filloni t'i përdorni ato, pyesni veten se si mund të bënit pa to më parë.

Mbylljet

Deri më tani, ne kemi diskutuar veçoritë e FP në kontekstin e gjuhëve "thjesht" funksionale - gjuhë që janë zbatimi i llogaritjes lambda dhe nuk përmbajnë veçori që kundërshtojnë sistemin formal të Kishës. Megjithatë, shumë veçori të gjuhëve funksionale përdoren jashtë llogaritjes lambda. Megjithëse zbatimi i sistemit aksiomatik është interesant nga pikëpamja programuese për sa i përket shprehjeve matematikore, ai mund të mos jetë gjithmonë i zbatueshëm në praktikë. Shumë gjuhë preferojnë të përdorin elementë të gjuhëve funksionale pa iu përmbajtur një doktrine të rreptë funksionale. Disa gjuhë të tilla (për shembull Common Lisp) nuk kërkojnë që variablat të jenë përfundimtare - vlerat e tyre mund të ndryshohen. Ata as nuk kërkojnë që funksionet të varen vetëm nga argumentet e tyre - funksionet lejohen të aksesojnë gjendjen jashtë fushëveprimit të tyre. Megjithatë, ato përfshijnë gjithashtu veçori të tilla si funksione të rendit më të lartë. Kalimi i një funksioni në një gjuhë jo të pastër është paksa i ndryshëm nga operacioni analog brenda llogaritjes lambda dhe kërkon një veçori interesante të quajtur mbyllje leksikore. Le të hedhim një vështrim në shembullin e mëposhtëm. Mos harroni se në këtë rast variablat nuk janë përfundimtare dhe funksioni mund të aksesojë variablat jashtë fushëveprimit të tij:

Funksioni makePowerFn (int power) (int powerFn (int base) (turn pow (bazë, power);) return powerFn;) Function Square = makePowerFn (2); katror (3); // kthen 9
Funksioni make-power-fn kthen një funksion që merr një argument dhe e ngre atë në një fuqi specifike. Çfarë ndodh kur përpiqemi të vlerësojmë katrorin (3)? Fuqia e ndryshueshme është jashtë fushëveprimit për powerFn sepse makePowerFn tashmë ka dalë dhe grupi i tij është shkatërruar. Si funksionon atëherë katrori? Gjuha duhet të ruajë vlerën e fuqisë në një farë mënyre që funksioni katror të funksionojë. Po sikur të krijojmë një funksion tjetër kub që e ngre numrin në fuqinë e tretë? Gjuha do të duhet të ruajë dy vlera të fuqisë për çdo funksion të krijuar në make-power-fn. Fenomeni i ruajtjes së këtyre vlerave quhet mbyllje. Mbyllja bën më shumë sesa ruan argumentet e funksionit të lartë. Për shembull, një mbyllje mund të duket si kjo:

Funksioni makeIncrementer () (int n = 0; int increment () (kthim ++ n;)) Funksioni inc1 = makeIncrementer (); Funksioni inc2 = makeIncrementer (); inc1 (); // kthen 1; inc1 (); // kthen 2; inc1 (); // kthen 3; inc2 (); // kthen 1; inc2 (); // kthen 2; inc2 (); // kthen 3;
Gjatë ekzekutimit, vlerat e n ruhen dhe numëruesit kanë qasje në to. Për më tepër, çdo numërues ka kopjen e vet të n, pavarësisht nga fakti se ato duhet të ishin zhdukur pas ekzekutimit të funksionit makeIncrementer. Si arrin përpiluesi ta përpilojë këtë? Çfarë po ndodh në prapaskenat e mbylljeve? Fatmirësisht kemi një pasim magjik.

Gjithçka është bërë mjaft logjike. Në shikim të parë, është e qartë se variablat lokale nuk u binden më rregullave të fushëveprimit dhe jetëgjatësia e tyre është e papërcaktuar. Natyrisht, ato nuk ruhen më në pirg - ato duhet të mbahen në grumbull. Prandaj, mbyllja bëhet si një funksion normal që diskutuam më herët, përveç që ka një referencë shtesë për variablat përreth:

Klasa some_function_t (SymbolTable parentScope; // ...)
Nëse mbyllja i referohet një variabli që nuk është në shtrirjen lokale, atëherë ai merr parasysh shtrirjen mëmë. Kjo eshte e gjitha! Mbyllja lidh botën funksionale me botën OOP. Sa herë që krijoni një klasë që ruan ndonjë gjendje dhe e kaloni diku, mbani mend mbylljet. Një mbyllje është thjesht një objekt që krijon "atribute" në fluturim, duke i nxjerrë ato jashtë fushëveprimit, kështu që ju nuk duhet ta bëni vetë.

Tani Cfare?

Ky artikull ecën vetëm majën e ajsbergut të programimit funksional. Ju mund të gërmoni më thellë dhe të shihni diçka vërtet të madhe, dhe në rastin tonë, gjithashtu të mirë. Në të ardhmen, kam në plan të shkruaj për teorinë e kategorive, monadat, strukturat funksionale të të dhënave, sistemin e tipit në gjuhët funksionale, multithreading funksional, bazat e të dhënave funksionale dhe shumë të tjera. Nëse mund të shkruaj (dhe të mësoj gjatë rrugës) rreth gjysmës së këtyre temave, jeta ime nuk do të humbet kot. Deri atëherë, Google- miku juaj besnik.

Artikujt kryesorë të lidhur