Ensiluokkainen toiminto - First-class function

In computer science , joka on ohjelmointikieli on sanottu ensimmäisen luokan toimintoja , jos se kohtelee toiminnot kuin ensimmäisen luokan kansalaisia . Tämä tarkoittaa sitä, että kieli tukee toimintojen siirtämistä argumentteina muille funktioille, niiden palauttamista muiden toimintojen arvoina ja niiden määrittämistä muuttujille tai tallennusta tietorakenteisiin. Jotkut ohjelmointikielen teoreetikot tarvitsevat tukea myös nimettömille toiminnoille (funktion literaaleille). Kielillä, joilla on ensiluokkaisia ​​toimintoja, toimintojen nimillä ei ole erityisasemaa; niitä käsitellään kuin tavallisia muuttujia, joilla on funktiotyyppi . Termin loi Christopher Strachey "ensiluokkaisina kansalaisina" 1960-luvun puolivälissä.

Ensiluokkaiset toiminnot ovat välttämättömiä toiminnalliselle ohjelmointityylille , jossa korkeamman tason toimintojen käyttö on vakiokäytäntö. Yksinkertainen esimerkki korkeamman tilattu toiminto on kartta funktio, joka ottaa, kuten sen argumentteja, funktio ja luettelo, ja palauttaa listan muodostettu levittämällä toiminto jokaisen jäsenen luettelon. Jotta kieli tukee karttaa , sen on tuettava funktion välittämistä argumenttina.

On tiettyjä täytäntöönpanon vaikeuksia ohimennen toimii väitteitä tai palauttamalla ne tulokset, erityisesti kun läsnä on ei-paikallisia muuttujia käyttöön sisäkkäisiä ja anonyymi toimintoja . Historiallisesti näitä kutsuttiin funarg -ongelmiksi , joiden nimi tuli funktion argumentista. Varhaisilla pakottavilla kielillä nämä ongelmat vältettiin joko tukematta funktioita tulostyypeinä (esim. ALGOL 60 , Pascal ) tai jättämällä pois sisäkkäiset funktiot ja siten ei-paikalliset muuttujat (esim. C ). Varhainen funktionaalinen kieli Lisp käytti dynaamisen laajuuden lähestymistapaa , jossa ei-paikalliset muuttujat viittaavat kyseisen muuttujan lähimpään määritelmään funktion suorituskohdassa sen sijaan, että se määriteltiin. Asianmukainen tuki leksikkäille ensiluokkaisille toiminnoille otettiin käyttöön kaaviossa, ja se edellyttää toimintojen viittausten käsittelyä sulkijoina paljaiden toimintojen osoittimien sijasta , mikä tekee roskien keräämisestä välttämättömän.

Käsitteet

Tässä osassa verrataan sitä, miten tietyt ohjelmointi-idiomit käsitellään funktionaalisella kielellä ensiluokkaisilla toiminnoilla ( Haskell ) verrattuna pakottavaan kieleen, jossa toiminnot ovat toisen luokan kansalaisia ​​( C ).

Korkeamman tason funktiot: funktioiden välittäminen argumentteina

Kielellä, jossa funktiot ovat ensimmäisen luokan kansalaisia, funktiot voidaan välittää argumentteina muille funktioille samalla tavalla kuin muut arvot (funktiota, joka ottaa toisen funktion argumentiksi, kutsutaan ylemmän tason funktioksi). Haskellin kielellä :

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

Kielet, joilla toiminnot eivät ole ensiluokkaisia, antavat usein mahdollisuuden kirjoittaa korkeamman tason funktioita käyttämällä toimintoja, kuten funktio-osoittimia tai edustajia . Kielellä C :

void map(int (*f)(int), int x[], size_t n) {
    for (int i = 0; i < n; i++)
        x[i] = f(x[i]);
}

Näiden kahden lähestymistavan välillä on useita eroja, jotka eivät liity suoraan ensimmäisen luokan toimintojen tukemiseen. Haskell -näyte toimii luetteloilla ja C -näyte matriiseilla . Molemmat ovat luonnollisimpia yhdistettyjä tietorakenteita kullakin kielellä, ja C -otoksen käyttäminen linkitetyillä luetteloilla olisi tehnyt siitä tarpeettoman monimutkaisen. Tämä selittää myös sen, että C-toiminto tarvitsee lisäparametrin (joka antaa taulukon koon.) C-toiminto päivittää taulukon paikallaan palauttamatta arvoa, kun taas Haskellin tietorakenteet ovat pysyviä (uusi luettelo palautetaan kun vanha jätetään koskemattomaksi.) Haskell -näyte käyttää rekursiota listan läpikäymiseen , kun taas C -näyte käyttää iteraatiota . Jälleen kerran, tämä on luonnollisin tapa ilmaista tämä toiminto molemmilla kielillä, mutta Haskell -näyte olisi voitu ilmaista helposti kertaiseksi ja C -näyte rekursioksi. Lopuksi Haskell -funktiolla on polymorfinen tyyppi, koska C ei tue tätä, olemme kiinnittäneet kaikki tyyppimuuttujat tyyppivakioon int.

Anonyymit ja sisäkkäiset toiminnot

Kielillä, jotka tukevat nimettömiä toimintoja, voimme välittää tällaisen funktion argumenttina korkeamman tason funktiolle:

main = map (\x -> 3 * x + 1) [1, 2, 3, 4, 5]

Kielellä, joka ei tue nimettömiä toimintoja, meidän on sitottava se nimen sijaan:

int f(int x) {
    return 3 * x + 1;
}

int main() {
    int list[] = {1, 2, 3, 4, 5};
    map(f, list, 5);
}

Ei-paikalliset muuttujat ja sulkemiset

Kun meillä on nimettömiä tai sisäkkäisiä toimintoja, heidän on luonnollista viitata kehonsa ulkopuolella oleviin muuttujiin (joita kutsutaan ei-paikallisiksi muuttujiksi ):

main = let a = 3
           b = 1
        in map (\x -> a * x + b) [1, 2, 3, 4, 5]

Jos funktiot esitetään paljailla funktio -osoittimilla, emme voi enää tietää, miten funktion rungon ulkopuolella oleva arvo tulisi siirtää sille, ja siksi sulkeminen on rakennettava manuaalisesti. Siksi emme voi puhua "ensiluokkaisista" toiminnoista täällä.

typedef struct {
    int (*f)(int, int, int);
    int *a;
    int *b;
} closure_t;

void map(closure_t *closure, int x[], size_t n) {
    for (int i = 0; i < n; ++i)
        x[i] = (*closure->f)(*closure->a, *closure->b, x[i]);
}

int f(int a, int b, int x) {
    return a * x + b;
}

void main() {
    int l[] = {1, 2, 3, 4, 5};
    int a = 3;
    int b = 1;
    closure_t closure = {f, &a, &b};
    map(&closure, l, 5);
}

Huomaa myös, että tämä mapon nyt erikoistunut toimintoihin, jotka viittaavat kahteen intympäristöönsä kuulumattomiin s. Tämä voidaan määrittää yleisemmin, mutta vaatii enemmän kattilalevykoodia . Jos fse olisi ollut sisäkkäinen toiminto, olisimme silti törmänneet samaan ongelmaan, ja siksi niitä ei tueta C.

Korkeamman tason toiminnot: funktioiden palauttaminen tuloksina

Kun palautat funktion, palaamme itse asiassa sen sulkemisen. C -esimerkissä kaikki sulkemisen siepatut paikalliset muuttujat menevät soveltamisalan ulkopuolelle, kun palaamme sulkemista rakentavasta funktiosta. Sulkemisen pakottaminen myöhemmässä vaiheessa johtaa määrittelemättömään käyttäytymiseen, mikä saattaa vioittaa pinon. Tämä tunnetaan ylöspäin suuntautuvana funarg -ongelmana .

Funktioiden määrittäminen muuttujille

Funktioiden määrittäminen muuttujille ja niiden tallentaminen (maailmanlaajuisiin) tietorakenteisiin kärsii mahdollisesti samoista vaikeuksista kuin funktioiden palauttaminen.

f :: [[Integer] -> [Integer]]
f = let a = 3
        b = 1
     in [map (\x -> a * x + b), map (\x -> b * x + a)]

Toimintojen yhtäläisyys

Koska useimpia kirjaimita ja arvoja voidaan testata tasa -arvon vuoksi, on luonnollista kysyä, voiko ohjelmointikieli tukea tasa -arvon testaustoimintoja. Jatkotarkastuksessa tämä kysymys näyttää vaikeammalta, ja on erotettava useat toiminnalliset tasa -arvotyypit:

Laaja tasa -arvo
Kaksi funktiota f ja g katsotaan laajasti yhtäläisiksi, jos ne sopivat kaikkien tulojensa ulostuloista (∀ x . F ( x ) = g ( x )). Tämän tasa -arvon määritelmän mukaan esimerkiksi mitä tahansa kahta vakaan lajittelualgoritmin toteutusta , kuten lisäyslajittelua ja yhdistämislajittelua , pidettäisiin yhtäläisinä. Laajennetusta tasa -arvosta päättäminen on päättämätöntä yleensä ja jopa toiminnoissa, joilla on rajalliset alueet, jotka ovat usein vaikeita. Tästä syystä mikään ohjelmointikieli ei toteuta toiminnallista tasa -arvoa laajuisena tasa -arvona.
Intensiivinen tasa -arvo
Intensionaalisen tasa -arvon mukaan kahta funktiota f ja g pidetään yhtäläisinä, jos niillä on sama "sisäinen rakenne". Tällainen tasa voitaisiin toteuttaa tulkita kielillä vertaamalla lähdekoodia funktion elinten (kuten tulkittuna Lisp 1.5) tai kohdekoodin on koottu kielillä . Intensionaalinen tasa -arvo merkitsee laajuista tasa -arvoa (olettaen, että toiminnot ovat deterministisiä ja niissä ei ole piilotettuja syötteitä, kuten ohjelmalaskuri tai muuttuva globaali muuttuja .)
Viite -tasa -arvo
Koska laajennetun ja intensiivisen tasa -arvon toteuttaminen on epäkäytännöllistä, useimmat tasa -arvon testaustoimintoja tukevat kielet käyttävät tasa -arvoa. Kaikille toiminnoille tai sulkemisille annetaan yksilöllinen tunniste (yleensä funktion elimen osoite tai sulkeminen) ja tasa -arvo päätetään tunnuksen yhtäläisyyden perusteella. Kaksi erikseen määriteltyä, mutta muuten identtistä funktion määritelmää pidetään eriarvoisina. Viittaava tasa -arvo merkitsee intensiivistä ja laajaa tasa -arvoa. Viitteellinen tasa -arvo rikkoo viittausten läpinäkyvyyttä, eikä sitä siksi tueta puhtailla kielillä, kuten Haskell.

Tyyppiteoria

In type Teoriassa , tarkoitetuista toiminnoista hyväksyä arvoja tyypin ja palauttamalla arvot tyypin B voidaan kirjoittaa → B tai B . Vuonna Curry-Howard vastaavuus , toiminto tyypit liittyvät looginen seuraus ; lambda -abstraktio vastaa hypoteettisten oletusten purkamista ja funktion sovellus modus ponens -päätössääntöä . Ohjelmointitoimintojen tavanomaisen tapauksen lisäksi tyyppiteoria käyttää myös ensiluokkaisia ​​funktioita assosiatiivisten matriisien ja vastaavien tietorakenteiden mallintamiseen .

In luokka-teoreettinen tilit ohjelmointi, saatavuus ensimmäisen luokan toimintojen vastaa suljettuun ryhmään oletus. Esimerkiksi yksinkertaisesti kirjoitettu lambda -laskenta vastaa Cartesian suljettujen luokkien sisäistä kieltä .

Kielten tuki

Toiminnallisilla ohjelmointikielillä, kuten Scheme , ML , Haskell , F# ja Scala , on kaikilla ensiluokkaisia ​​toimintoja. Kun Lisp , yksi varhaisimmista funktionaalisista kielistä, suunniteltiin, kaikki ensimmäisen luokan toimintojen osa-alueet eivät sitten olleet oikein ymmärrettyjä, mikä johti toimintojen dynaamisuuteen. Myöhemmissä Scheme- ja Common Lisp -murteissa on leksikaalisesti määriteltyjä ensiluokkaisia ​​toimintoja.

Monilla skriptikielillä, kuten Perl , Python , PHP , Lua , Tcl /Tk, JavaScript ja Io , on ensiluokkaisia ​​toimintoja.

Pakollisten kielten osalta on tehtävä ero Algolin ja sen jälkeläisten, kuten Pascalin, perinteisen C-perheen ja nykyaikaisten roskakoristeiden välillä. Algol-perhe on sallinut sisäkkäiset funktiot ja ylemmän asteen ottofunktion argumentteina, mutta ei korkeamman asteen toimintoja, jotka palauttavat funktioita tuloksina (paitsi Algol 68, joka sallii tämän). Syynä tähän oli se, että ei tiedetty, miten käsitellä muita kuin paikallisia muuttujia, jos sisäinen funktio palautettiin tämän seurauksena (ja Algol 68 tuottaa tällaisissa tapauksissa ajonaikaisia ​​virheitä).

C -perhe salli molempien toimintojen siirtämisen argumentteina ja niiden palauttamisen tuloksina, mutta välttyi ongelmilta tukematta sisäkkäisiä toimintoja. (Gcc-kääntäjä sallii ne laajennuksena.) Koska funktioiden palauttamisen hyödyllisyys perustuu ensisijaisesti kykyyn palauttaa sisäkkäisiä funktioita, jotka ovat tallentaneet ei-paikallisia muuttujia, ylimmän tason toimintojen sijaan näillä kielillä ei yleensä katsota olevan ensimmäistä -luokan toiminnot.

Nykyaikaiset pakottavat kielet tukevat usein roskien keräystä, mikä tekee ensimmäisen luokan toimintojen toteuttamisesta mahdollista. Ensiluokkaisia ​​toimintoja on usein tuettu vain kielen myöhemmissä versioissa, mukaan lukien C# 2.0 ja Applen Blocks-laajennus C, C ++ ja Objective-C. C ++ 11 on lisännyt tuen nimettömille toiminnoille ja kielen sulkemisille, mutta koska kieltä ei kerätä roskakoriin, on kiinnitettävä erityistä huomiota siihen, että funktioiden ei-paikalliset muuttujat palautetaan tuloksina (katso alla ).

Kieli Korkeamman tason toiminnot Sisäkkäiset toiminnot Ei-paikalliset muuttujat Huomautuksia
Argumentit Tulokset Nimetty Anonyymi Sulkeminen Osittainen sovellus
Algolin perhe ALGOL 60 Joo Ei Joo Ei Alaspäin Ei Onko toiminnotyyppejä .
ALGOL 68 Joo Joo Joo Joo Alaspäin Ei
Pascal Joo Ei Joo Ei Alaspäin Ei
Ada Joo Ei Joo Ei Alaspäin Ei
Oberon Joo Vain sisäkkäin Joo Ei Alaspäin Ei
Delfoi Joo Joo Joo 2009 2009 Ei
C perhe C Joo Joo Kyllä GNU C: ssä Ei Ei Ei Sisältää toiminto -osoittimia .
C ++ Joo Joo C ++ 11 C ++ 11 C ++ 11 C ++ 11 Sisältää toiminto -osoittimia, toiminto -objekteja . (Katso myös alla.)

Selkeä osittainen käyttö mahdollista std::bind.

C# Joo Joo 7 2.0 / 3.0 2.0 3.0 Sisältää edustajia (2.0) ja lambda -lausekkeita (3.0).
Tavoite-C Joo Joo Käyttämällä nimettömiä 2.0 + lohkot 2.0 + lohkot Ei Sisältää toiminto -osoittimia.
Java Joo Joo Käyttämällä nimettömiä Java 8 Java 8 Ei Sisältää nimettömiä sisäluokkia .
Mennä Joo Joo Käyttämällä nimettömiä Joo Joo Joo
Limbo Joo Joo Joo Joo Joo Ei
Newsqueak Joo Joo Joo Joo Joo Ei
Ruoste Joo Joo Joo Joo Joo Joo
Toiminnalliset kielet Lisp Syntaksi Syntaksi Joo Joo Yhteinen Lisp Ei (Katso alempaa)
Kaavio Joo Joo Joo Joo Joo SRFI 26
Julia Joo Joo Joo Joo Joo Joo
Clojure Joo Joo Joo Joo Joo Joo
ML Joo Joo Joo Joo Joo Joo
Haskell Joo Joo Joo Joo Joo Joo
Scala Joo Joo Joo Joo Joo Joo
F# Joo Joo Joo Joo Joo Joo
OCaml Joo Joo Joo Joo Joo Joo
Skriptikielet Io Joo Joo Joo Joo Joo Ei
JavaScript Joo Joo Joo Joo Joo ECMAScript 5 Osittainen sovellus mahdollinen käyttäjän maakoodilla ES3: ssa
Lua Joo Joo Joo Joo Joo Joo
PHP Joo Joo Käyttämällä nimettömiä 5.3 5.3 Ei Osittainen sovellus mahdollinen käyttäjän maakoodilla.
Perl Joo Joo 6 Joo Joo 6
Python Joo Joo Joo Vain lausekkeet Joo 2.5 (Katso alempaa)
Rubiini Syntaksi Syntaksi Tarkastamaton Joo Joo 1.9 (Katso alempaa)
Muut kielet Fortran Joo Joo Joo Ei Ei Ei
Vaahtera Joo Joo Joo Joo Joo Ei
Mathematica Joo Joo Joo Joo Joo Ei
MATLAB Joo Joo Joo Joo Joo Joo Osittainen sovellus mahdollista luomalla uusia toimintoja automaattisesti.
Rupattelu Joo Joo Joo Joo Joo Osittainen Osittainen hakemus mahdollista kirjaston kautta.
Nopea Joo Joo Joo Joo Joo Joo
C ++
C ++ 11- sulkimet voivat kaapata ei-paikallisia muuttujia kopiorakenteella, viitteellä (pidentämättä niiden käyttöikää) tai siirtorakenteella (muuttuja elää niin kauan kuin sulkeminen). Ensimmäinen vaihtoehto on turvallinen, jos sulkeminen palautetaan, mutta vaatii kopion, eikä sitä voida käyttää alkuperäisen muuttujan muokkaamiseen (jota ei ehkä ole enää olemassa sulkemisen kutsumishetkellä). Toinen vaihtoehto välttää mahdollisesti kalliin kopion ja mahdollistaa alkuperäisen muuttujan muokkaamisen, mutta se ei ole turvallinen, jos sulkeminen palautetaan (katso roikkuvat viitteet ). Kolmas vaihtoehto on turvallinen, jos sulkeminen palautetaan eikä vaadi kopiota, mutta sitä ei voida käyttää myöskään alkuperäisen muuttujan muokkaamiseen.
Java
Java 8: n sulkemiset voivat kaapata vain lopullisia tai "tehokkaasti lopullisia" ei-paikallisia muuttujia. Javan funktiotyypit esitetään luokina. Anonyymit toiminnot käyttävät kontekstista pääteltyä tyyppiä. Menetelmäviittauksia on rajoitetusti. Lisätietoja on kohdassa Anonyymi toiminto § Java -rajoitukset .
Lisp
Lexisesti laajennetut Lisp -versiot tukevat sulkemista. Dynaamisesti laajennetut vaihtoehdot eivät tue sulkimia tai tarvitsevat erikoisrakenteen sulkimien luomiseksi.
In Common Lisp , tunniste funktion toiminto nimiavaruuden ei voida käyttää viittaus ensimmäisen luokan arvo. Erikoisoperaattoria functionon käytettävä funktion noutamiseen arvona: (function foo)arvioi funktio -objektiksi. #'fooon olemassa lyhenteenä. Soveltaa tällaista toimintoa esine, yksi on käytettävä funcalltoiminto: (funcall #'foo bar baz).
Python
Selkeä osittainen sovellus functools.partialversiosta 2.5 ja operator.methodcallerversiosta 2.6 lähtien.
Rubiini
Säännöllisen "toiminnon" tunnusta Rubyssa (joka on todella menetelmä) ei voida käyttää arvona tai välittää. Se on ensin haettu osaksi Methodtai Procesine, jota käytetään ensimmäisen luokan tiedot. Tällaisen funktio -objektin kutsumisen syntaksi eroaa tavallisista menetelmistä.
Sisäkkäisten menetelmien määritelmät eivät todellakaan sisällä soveltamisalaa.
Selvää currya [1].

Katso myös

Huomautuksia

Viitteet

Ulkoiset linkit