A Digitalmars D programozási nyelv

Alprogramok, modulok

Modulok

A definíció BNF-ben:

Module:
	ModuleDeclaration DeclDefs
	DeclDefs

DeclDefs:
	DeclDef
	DeclDef DeclDefs

DeclDef:
	AttributeSpecifier
	ImportDeclaration
	EnumDeclaration
	ClassDeclaration
	InterfaceDeclaration
	AggregateDeclaration
	Declaration
	Constructor
	Destructor
	Invariant
	UnitTest
	StaticConstructor
	StaticDestructor
	DebugSpecification
	VersionSpecification
	MixinDeclaration
	;
Megj: A fenti BNF egy teljes modul szerkezetét írja le. Ebben a fejezetben csak a modulokkal kapcsolatos deklarációkkal, importálással, valamint a statikus konstruktorokkal, destruktorokkal foglalkozunk. Látszik, hogy a ModuleDeclaration résszel kell kezdődni egy modulnak. Azonban a többi rész sorrendje szinte tetszőleges. A nem tárgyalt részegységeket ld. a megfelelő fejezetekben.

Minden modul egy forrásfájlnak felel meg. Azaz minden forrásfájl egy modul. A modul neve - ha nem adtuk meg (ld. később) - megegyezik a fájl nevével a kiterjesztést és az elérési utat elhagyva.

Egy modul automatikusan egy új névteret deklarál, és a modul tartalma ebben a névtérben lesz elérhető. A modulok látszólag nagyon hasonlítanak az osztályra, azonban nagyon sok különbség van köztük. Ezek:

A modulok viszont hierarchiába szervezhetőek, mégpedig a csomagok (packages) révén.

A modulokkal kapcsolatos szabályok a következőket biztosítják:

Modul-deklaráció

A ModuleDeclaration (BNF-ben) megadja a modul nevét, valamint azt, hogy melyik csomagba (package) tartozik. Ha a név hiányzik, akkor a tartalmazó fájl neve lesz kiterjesztés és elérési útvonal nélkül.

BNF:

ModuleDeclaration:
	module ModuleName ;

ModuleName:
	Identifier
	ModuleName . Identifier

A legjobboldalibb Identifier (BNF) a modul neve (ezután már nincs pont). Az azt megelőző Identifier-ek azoknak a csomagoknak (packages) nevei, melyek az aktuális modult tartalmazzák. A csomagok nevei megfelelnek a könyvtárak neveinek, amelyek a modul elérési útvonalában vannak. A csomagnevek nem lehetnek foglalt a nyelv kulcsszavai, ezáltal a könyvtárnevek sem lehetnek kulcsszavak.

Ha meg van adva, a ModuleDeclaration (BNF) résszel kell kezdődni a fájlnak, és csak egy ilyen rész lehet.

Példa:

module c.stdio; //jelentése: az stdio modul a c csomagban

Konvenció szerint a csomag és modul nevek kisbetűsek. Ennek oka, hogy ezek egy az egyben megfelelnek a fájl és könyvtárneveknek, viszont bizonyos operációs rendszerek nem különböztetik meg a kis- és nagybetűt. Megj: A készítők szerint tartsuk be. Ennek oka a következő: az implementációk nem garantálják az ebből adódó problémák feloldását.

Import-deklaráció

Másik modulból származó Szimbólumokat az ImportDeclaration (BNF) részben adjuk meg.

BNF:
ImportDeclaration:
	import ImportList ;
	static import ImportList ;

ImportList:
	Import
	ImportBindings
	Import , ImportList

Import:
	ModuleName
	ModuleAliasIdentifier = ModuleName

ImportBindings:
	Import : ImportBindList

ImportBindList:
	ImportBind
	ImportBind , ImportBindList

ImportBind:
	Identifier
	Identifier = Identifier

ModuleAliasIdentifier:
	Identifier

Az ImportDeclaration részt sokféleképpen adhatjuk meg: a teljesen általánostól az egészen részletesig.

Az ImportDeclarations részek megadásának sorrendje tetszőleges.

A ModuleName részeknek az ImportDeclaration-ban teljesen meghatározottnak kell lenniük. Ez a nyelv elvárása, gyakorlatban ez azt jelenti, hogy meg kell adnunk a csomago(ka)t (package), amely (ek)ben az általunk importálni kívánt modul megtalálható. A csomagok megadása abszolút (emlékeztető: ez egy útvonalnak felel meg a fájlrendszerben). Tehát nem számít, hogy melyik modulból importálunk, ugyanazokat a csomagokat kell felsorolni.

Egyszerű importálás

A legegyszerűbb módja az importálásnak az, ha egyszerűen felsoroljuk a modulokat, melyeket importálni szeretnénk:

import std.stdio; // az stdio modul importálása az std csomagból import foo, bar; // a foo és a bar modul importálása void main () { writefln ("hello!\n"); // ezt hívja meg: std.stdio.writefln }

Az egyszerű importálás a tehát a következőképpen működik. Először egy nevet az aktuális névtérben keres a rendszer. Ha itt nem találja, utána veszi csak figyelembe az importokat. Ha pontosan egyet talál a névből, akkor azt használja. Ha egynél több importban találja meg a nevet (vagy triviálisan, ha nem találja), akkor az hiba.

module A; void foo (); void bar ();

module B; void foo (); void bar ();

module C; import A; void foo (); void test () { foo (); // C.foo () -t hívja meg, mert az aktuális névtérben megtalálta bar (); // A.bar () -t hívja meg, mivel az importok között megvan (az A modulban) }

module D; import A; import B; void test () { foo (); //hiba, A.foo () vagy B.foo () ? A.foo (); // rendben, A.foo () B.foo (); // rendben, B.foo () }

module E; import A; import B; alias B.foo foo; void test () { foo (); // B.foo () hívása A.foo (); // A.foo () hívása B.foo (); // B.foo () hívása }

Publikus Importok

Alapértelmezésben minden import privát (private). Ez azt jelenti, hogy ha az A modul importálja B modult, és B pedig C-t, akkor az A-ban C neveit már nem veszi figyelembe a fordító. Viszont egy importot deklarálhatunk publikusnak is public, ekkor a C úgy fog látszani A-ból, mintha közvetlenül importáltuk volna.

module A; void foo () { }

module B; void bar () { }

module C; import A; public import B; ... foo (); // call A.foo () bar (); // calls B.bar ()

module D; import C; ... foo (); // error, foo () is undefined bar (); // ok, calls B.bar ()

Statikus Importok

Az egyszerű importálás kétségtelenül jól működik kevés modullal. De ha sok modul van, könnyen előfordulhatnak névütközések, sőt többnyire elő is fordulnak. Főleg olyan gyakori függvényneveknél, mint read, write stb. Egyik megoldás a statikus importálás használata. Statikus importálással ez a probléma egyszerűen (Megj:ez egy kicsit brute force megoldás.)kezelhető, ekkor minden nevet csak abszolút adhatunk meg. Azaz a csomagok és végül a modul neve ponttal elválasztva, és csak ez után a név. Pl:

static import std.stdio; void main () { writefln ("hello!"); // hiba, a writefln nincs deklarálva std.stdio.writefln ("hello!"); // így már rendben }

Átnevezett Importok

Egy importnak akár adhatunk lokális nevet is, így kerülve el a névütközést. Ezt az alábbi módon lehet megtenni, és ezután már CSAK az új név használható:

import io = std.stdio; void main () { io.writefln ("hello!"); // ok std.stdio.writefln ("hello!"); // hiba, az eredeti név nem érvényes már writefln ("hello!"); // így sem lesz jó }

Tanács: nagyon hosszú importneveknél jól jöhet, hasonlóan, mint egy typedef.

Szelektív Importálás

Szimbólumokat (azaz pl. függvény nevet) egyesével is importálhatunk és köthetjük az aktuális névtérhez:

import std.stdio : writefln, foo = writef; void main () { std.stdio.writefln ("hello!"); // hiba, std-t nem hoztuk be writefln ("hello!"); // ok, writefln az aktuális névtérben van már writef ("world"); // hiba, writef át van nevezve - foo ("world"); // - és itt már helyesen használjuk fwritefln (stdout, "abc"); // ez is hiba, fwritefln-t sehol nem importáltuk }

Statikus importok nem vegyíthetőek a szelektív importokkal. Megj: Ennek nem is lenne értelme, mert importáljuk az egész modult statikusan, és akkor úgyis minden szimbólumra csak minősített névvel hivatkozhatnánk.

Szelektív Import Átnevezése

Ezt a kettőt viszont már kombinálhatjuk, a szintaxis (Megj: talán nem túl szerencsés az alábbi:

import io = std.stdio : foo = writefln; void main () { writefln ("bar"); // ez hiba, mivel writefln sehol nincs importálva, hanem átneveztük std.stdio.foo ("bar"); // ez azért nem jó, mert az új foo szimbólumot csak az aktuális névtérhez kötöttük std.stdio.writefln ("bar"); // hiba, std.stdio importjából nem következik std importja (ami egy csomag) foo ("bar"); // ez így már jó: foo az aktuális névterünkben van io.writefln ("bar"); // ok, io=std.stdio egy átnevezés io.foo ("bar"); // a foo, mint szimbólum, nem tagja io-nak

Megj: a fenti példából látszik, hogy egy szimbólum nem azonos az entitással, amire mutat. Tehát például egy függvénynek lehet több neve is. Képzeljük el úgy, mintha az összes függvény fel lenne sorolva egy táblázatban, sorszámozva, és a nevek ezekre az entitásokra mutatnak. A D nyelv persze nem így van megadva, ez a példa csak a megértést segíti.

Modul hatásköre és hatáskör operátor

Néha szükséges, hogy felüldefiniáljuk a szokásos hatóköri szabályokat, hogy hozzáférjünk egy rejtett lokális névhez. Ezt a globális hatókör operátorral tehetjük meg (global scope operator), ami a pont karakter: '.'. A pont kerül legelőre:
int x; int foo (int x) { if (y) return x; // returns foo.x, not global x else return .x; // returns global x }
A pont hatására a fordító az aktuális modul hatáskörében keresi az x változót.

Statikus konstruktor és destruktor

A szintaxis így néz ki:
static int a; static int b=1; static this () { a=5; b=a+5; }

A statikus konstruktor arra való, hogy még a main () függvény meghívása előtt inicializáljon egy modult, vagy egy osztályt. A statikus destruktorok pedig a main () visszatérése után futnak le. Általában rendszer-erőforrások elengedésére használjuk.

Csak statikus változókat érhetnek el, de pontosan ez a céljuk. Némely nyelvekben ezt implicite oldják meg:

static int a = b + 1; static int b = a * 2; //Ha a fenti példában ezt írtuk volna: ... static int b = a+4; ... //Akkor az hiba, mivel csak konstans megengedett, a stat. konstruktorok vannak e helyett //alapértelmezésben a egyébként 0-ra inicializálódik (minden típusra meg van adva egy ilyen érték)

Szerepelhet közvetlenül egy modul törzsében, vagy egy osztályban.

Egy modulban a statikus konstruktorból és destruktorból több is lehet. A statikus konstruktorok lexikális sorrendben futnak le, a statikus destruktorok fordított lexikális sorrendben. Ezt a lexikális sorrendet a modul, ill. osztály nevek adják.

A paraméterlista a konstruktornál és a destruktornál is üres. Nem is lenne értelme, mivel ezeket a függvényeket semmi sem hívhatja meg, ill. a futtatókörnyezet hívja meg őket.

Példa helyes és helytelen megadási módra:
class Foo { static this () { ... } // helyes static private this () { ... } // nem lehet hozzáférési szintje static { this () { ... } // ez mi?? ilyen sincs... } static: this () { ... } // nem, ilyen vagy hasonló C++ -ban sincs }

A statikus konstruktorok lefutási sorrendje

Minden egyes modulban a statikus inicializáció sorrendjét implicite meghatározzák az import deklarációk. A fordító feltételezi, hogy egy modul függ minden olyan importált modultól, amelynek előbb fut le a statikus konstruktora. Ezen kívül nincs más szabály a statikus konstruktorok lefutási sorrendjére. Tehát modulon belül lexikális sorrend; és az összes modulnak, amelyektől függünk, le kell futnia a stat. konstruktorának, mielőtt az aktuális modult elkezdenénk.

Körkörös függőségek megengedettek. Ha a körön belül több modulnak is van statikus konstruktora vagy destruktora, akkor futási idejű kivétel keletkezik.Megj: ha csak lehet, kerüljük a körkörös függőségeket, mert megnehezítik a karbantartást. Pl: lehet, hogy az egyik modulnak még nincs stat. Konstruktora, de egyik munkatársunk úgy gondolja, hogy legyen. Ez az egész projektre kiterjedő problémákat okozhat.

Modulon belül a statikus konstruktorok sorrendje

Tehát még egyszer: lexikális sorrendben.

Statikus destruktorok sorrendje

Fordított sorrendben, mint a konstruktorok. Csak akkor fut le egy statikus destruktor, ha a megfelelő stat. konstruktor sikeresen lefutott.

Unit tesztek sorrendje

A Unit tesztek a statikus konstruktorok lefutása után, de a main () előtt futnak le. Ezek szintén lexikális sorrendben futnak le. Unit tesztekről részletesebben.

Alprogramok

A D nyelvben alapvetően ugyanúgy lehet függvényeket definiálni, mint C/C++-ban.
Különbség, hogy lehetőség van függvényeket egymásba ágyazni. Ekkor a beágyazott függvény elérheti a befoglaló függvény változóit, valamint a további beágyazott függvényeket. Egy beágyazott függvény természetesen csak azon a függvényen belül használható, melyben a definíciója található, azon kívül nem látható.

Az alprogramok további speciális jellemzőit a következő alpontokban foglaljuk össze.

Paraméterátadás

A D nyelvben alapvetően érték szerinti paraméterátadás van, amit a következő példa szemléltet:

void fun(int x) { x += 42; } void gun(int[] x) { x = [ 1, 2, 3 ]; } void hun(int[] x) { x[0] = x[1]; } unittest { int x = 10; fun(x); assert(x == 10); // Unchanged int[] y = [ 10, 20, 30 ]; gun(y); assert(y == [ 10, 20, 30 ]); // Unchanged hun(y); assert(y == [ 20, 20, 30 ]); // Changed! }

Ahogy a fenti kódrészletből is látható, a függvény paraméterének megváltoztatása nem érzékelhető a függvényen kívül. Egyedül a hun függvényben lévő értékadás észlelhető az eredeti változón is, mivel a tömbök, a C-hez hasonlóan referencia szerint kerülnek átadásra. Pontosabban a tömbök eleve referenciák, és ezek kerülnek átadásra érték szerint. Így a referencia által hivatkozott memóriaterület megváltoztatása látható a függvényen kívül is.

A D nyelvben különböző módosítókkal határozhatjuk meg a paraméterátadás módját. Ezek a következők:

ref paraméterek

Lehetőség van nem csak a referencia-típusú, hanem az érték-típusú változókat is referencia szerint átadni, erre szolgál a ref módosító. Ekkor a paraméter in-out szemantikájú lesz. Az eredeti értéke elérhető a függvényben, és a függvényen belül megváltoztatott érték látható a függvényen kívül is.

void bump(ref int x) { ++x; } unittest { int x = 1; bump(x); assert(x == 2); }

Természetesen ilyen esetben csak balérték lehet az aktuális paraméter, így a következő függvényhívás fordítási hibát okoz:

bump(5);

A ref módosítót nem csak a formális paraméterek megadásánál, hanem a visszatérési érték típusának definiálásánál is használhatjuk. Ekkor a visszaadott érték is balérték lesz. Így megjelenhet olyan helyeken, ahol balértékre van szükség, például egy ref paraméter aktuális értékeként.

ref int bump(ref int x) { return ++x; } unittest { int x = 1; bump(bump(x)); // Two increments assert(x == 3); }
in paraméterek

Az in módosítóval ellátott formális paraméterek bemenő szemantikájú paramétereket definiálnak. Ez azt jelenti, hogy a paramétert nem változtathatjuk meg a függvényen belül, az konstans érték lesz. Mivel biztosított, hogy az ilyen paramétereket a függvény csak olvashatja, nem szükséges másolatot készíteni róla a függvény számára, így gyorsabbá tehető a függvény meghívása.
Az adat módosítása elleni védelem tranzitív, tehát például egy tömb elemeit sem módosíthatjuk, ha a tömb in módosítóval ellátott paramétere a függvénynek.

void fun(in int[][] data) { data[5] = data[0]; // Error! Cannot modify 'in' parameter data[5][0] = data[0][5]; // Error! Cannot modify 'in' parameter }
out paraméterek

Az out módosítóval definiált paraméterek kimenő szemantikával rendelkeznek, azaz ezeken keresztül a függvény nem jut információhoz, hanem információt juttat a külvilágba. Az ilyen paraméterek a függvény lokális változói, melyek ennek megfelelően az adott típus alapértékével inicializálódnak. A függvény végrehajtása során ezeket ugyanúgy lehet olvasni és írni, mint a többi változót. A függvény végén a paraméterben lévő érték bemásolódik a függvény aktuális paraméterébe. Éppen ezért az out módosítóval ellátott formális paramétereknek csak balérték feleltethető meg aktuális paraméterként.

Az kimenő paraméterek helyett általában használhatók lennének a ref módosítóval ellátott paraméterek is, azt figyelembe véve, hogy amíg a függvényen belül nem adunk értéket a paraméternek, addig ne olvassuk, mivel kintről származó információt tárolhat. Azonban a kód megértése szempontjából jobban kifejezi a programozói szándékot, ha minden esetben, amikor a paramétert csak kimenő szemantikával akarjuk használni, akkor out módosítóval lássuk el a ref helyett. Erre egy példa a következő függvény:

// Computes divisor and remainder of a and b // Returns divisor by value, remainder in the 'rem' parameter int divrem(int a, int b, out int rem) { assert(b != 0); rem = a % b; return a / b; } unittest { int r; int d = divrem(5, 2, r); assert(d == 2 && r == 1); }
scope paraméterek

A scope módosítóval ellátott paramétereket nem lehet kivinni a hatókörből, azaz például nem rendelhetjük őket egy globális változóhoz. Hasznos lehet, ha egy alprogramnak paraméterként szeretnénk átadni egy erőforrást kezelő objektumot. Ekkor ha az alprogram kivinné a hatókörből az objektumot, bizonytalanná válna, hogy mikor hívódik meg a destruktor, azaz mikor szabadul fel az erőforrás. A scope módosító garantálja, hogy a függvényünk nem okoz erőforrás-szivárgást. (Persze ettől még az őt hívó függvény okozhat problémákat.)

Lusta kiértékelés

Bemenő szemantikájú paraméterek esetén egy lehetséges módosító a lazy, mely hatására az adott paraméter kiértékelése lusta lesz. Ekkor a paraméter értékét csak olvasni lehet a függvényen belül. Akkor hasznos, ha a paramétert egy komplex számítás eredménye adja, melyre lehet, hogy nem is lesz szükség a függvényben. Ekkor a lustaság miatt a paraméter nem kerül kiértékelésre, csak ha valóban szükség lesz rá.

Egy egyszerű példa a következő: Adott egy naplózó függvény, mely a paraméterként kapott karakter-sorozatot kiírja egy fájlba, de ezt csak akkor kell megtennie, ha a globális logging változó értéke true. Ha a logging értéke false, akkor a szövegre igazából nincs is szükség. Ilyen esetben a következő módon adhatjuk meg a függvényt és egy hívását:

void log(lazy char[] dg) { if (logging) writefln(logfile, dg()); } void foo(int i) { log("Entering foo() with i set to " ~ toString(i)); }

A lazy paraméterek helyett a fordító delegátokat készít, melyek az aktuális paramétert adják értékül, így azok értéke nem kerül meghatározásra a függvény hívásakor. (A delegátokról és alprogram-literálokról jelen fejezet következő részei tartalmaznak részletes ismertetést.) Az előző példából a következőt készíti a fordító a lazy módosító eltávolításakor:

void log(char[] delegate() dg) { if (logging) fwritefln(logfile, dg()); } void foo(int i) { log( { return "Entering foo() with i set to " ~ toString(i); }); }

Mivel a lusta paraméterekből delegátok készülnek, lehetőség van bonyolultabb módon is kihasználni ezt a paraméterátadási technikát:

void dotimes(int count, lazy void exp) { for (int i = 0; i < count; i++) exp(); } void foo() { int x = 0; dotimes(10, writef(x++)); }

Ebben az esetben a writef(x++) kifejezésből készül egy delegát, melyet count-szor fog meghívni a dotimes függvény. Így a fenti kódrészlet által generált kimenet a következő:

0123456789

Függvények és delegátok

A függvények és delegátok közti különbség megértéséhez induljunk ki egy C++-beli példából. Adott egy globális függvény és egy osztály tagfüggvénye. A globális függvény végrehajtásához elegendő ismerni a függvény kódjának címét, hiszen a paraméterein kívül legfeljebb globális változókat ér el, de azok statikusak, így a kódban közvetlenül megjelennek. Egy osztály tagfüggvényét csak az osztály egy objektumára lehet végrehajtani, tehát beszéljünk egy objektum tagfüggvényéről. Itt nem elég az explicit paraméterek mellett a függvény címének ismerete, hiszen szükség van magára az objektumra is, mely a függvény implicit, this paramétere. Vagyis két mutatóra van szükség egy objektum tagfüggvényének tárolásához: a függvény kódját és az adott objektumot hivatkozó mutatókra. Ez utóbbi esetben beszélhetünk delegátról.

A D-ben a függvények egymásba ágyazhatók, és osztályok is beágyazhatók függvényekbe, így kicsit bonyolódik a helyzet. De az előbbi példát kicsit általánosítva adódik a függvény és delegát (delegate) fogalma.
Az olyan függvényeket, melyek vagy csak globális változókat használnak, vagy tiszta függvények, azaz a paramétereiken kívül nincs szükségük más változók értékére, nevezzük függvénynek. Azokat a függvényeket, melyeknek szükségük van a paraméterükön kívül egyéb dinamikus változók értékeire is, nevezzük delegátoknak. A delegátok lehetnek a C++-os példán bemutatott tagfüggvényeken kívül tipikusan olyan beágyazott függvények, melyek használják a befoglaló függvény lokális változóit.

A függvények és delegátok megkülönböztetése nem csak elméleti szempontok miatt szükséges, hanem, mint a példán is láttuk, tárolásuk különbözősége miatt gyakorlati alapjai is vannak. A függvényeket egyetlen mutató határozza meg, míg a delegátokhoz két mutató tárolása szükséges, a végrehajtandó utasításokra és a végrehajtáshoz szükséges környezetre hivatkozó mutatóé.

Az alprogramokra hivatkozó mutatókat a következő szintaxissal deklarálhatjuk, és használhatjuk:

class C { void foo() {} } void fun() {} unittest { C c = new C(); void function() fp; //function pointer declaration fp = &c.foo; //Error! Cannot implicitly convert delegate to function fp = &fun; //ok fp(); //call fun() void delegate() dg; // delegate pointer declaration dg = &fun; //Error! Cannot implicitly convert function to delegate dg = &c.foo; //ok dg(); //call c.foo() }

Típuskényszerítéssel (castolással) a delegátokat értékül adhatjuk függvény mutatóknak, de ez futási idejű hibához fog vezetni, hacsak a vezérlés el nem kerüli a környezetből származó változókhoz való hozzáféréseket. Tehát jól átgondolt módon lehet élni ezzel a lehetőséggel. A függvényekből delegátot viszont nem lehet készíteni, az ilyen típuskényszerítés fordítási hibához vezet.

A fenti deklarációkban látható szintaxissal paraméterként is átadhatunk függvényeket és delegátokat, sőt visszatérési értékként is megjelenhetnek. Ilyen formán a D nyelv magasabb rendű függvényeket (higher-order functions) biztosít a programozók számára.

Alprogram-literálok

Az alprogram-literálokkal név nélküli alprogramokat definiálhatunk. Az előző pontban ismeretett megkülönböztetésnek megfelelően két típusuk létezik: függvény-literálok és delegát-literálok.
Az alprogram-literáloknak csak a paramétereit kell megadni, a visszatérési típusukat a fordító kikövetkezteti. A függvény-literálokat a function, a delegát-literálokat a delegate kulcssszó vezeti be, melyek el is maradhatnak. Ha az alprogram-literált a paraméterlistával vezetjük be, akkor a fordító delegátnak fogja tekinteni. Amennyiben kitesszük a function kulcsszót, a fordító ellenőrzi, hogy literál törzsének végrehajtása során valóban nincs szükség a literál környzetére.
A paraméterlistában megadhatók a paraméterek típusai, de teljesen el is hagyhatók. Utóbbi esetben a fordító a paraméterek használata alapján kikövetkezteti azok típusát.

Nézzük a következő példát a literálok használatára:

T[] find(alias pred, T)(T[] input) if(is(typeof(pred(input[0])) == bool)) { for(;input.length > 0; input = input[1..$]) if(pred(input[0])) break; return input; } unittest { int[] a = [1, 0, -1]; assert(find!(function(x){return x < 0;})(a) == [-1]); int z = 1; assert(find!(delegate(x){return x < z;})(a) == [0,-1]); }

A fenti példában a második literált nem lehetne a function kulcsszóval bevezetni, mert a lokális z változó értékét is felhasználja.

Függvény- és delegát-literálok esetén elhagyható a bevezető kulcsszó, ahogy azt fentebb is említettük, de ezenfelül üres paraméterlista esetén még a paramétereklistát tartalmazó zárójelpárt sem kell kitenni. Ahhoz hasonlóan, hogy az üres paraméterlistával rendelkező függvény hívásakor sem kötelező kitenni az üres zárójelpárt.
Tehát a következő három literál ekvivalens:

void function() fp; fp = function(){return 0;} fp = (){return 0;} fp = {return 0;}

Megjegyzendő, hogy függvény- és delegát-mutatóknak való értékadáskor az alprogram-literálok elé nem kell referencia-operátor (&), ahogy az a fenti példában is látható.

Lambda kifejezések

Ha az alprogram-literálunk törzse csak egy return utasításból áll, akkor használhatunk lambda kifejezést. A lambda kifejezésben megadjuk, hogy adott paraméterekhez milyen kifejezést rendelünk, így a return utasítást nem kell kiírnunk. Az alprogram-literálokhoz hasonlóan a paraméterek típusát a fordító kikövetkezteti, ha az egyértelmű.

A példában egy tömb elemeit növeljük a duplájára függvény-literál és lambda kifejezés segítségével.

import std.array : map; void main() { int input[] = [1, 2, 3, 4]; map!((n) { return 2 * n; })(input); map!(n => 2 * n)(input); }

Tiszta függvények

Tiszta függvénynek az olyan függvényeket nevezzük, melyek matematikai értelemben vett függvények, azaz egy adott paraméterezésre mindig ugyanazt az eredményt adják. A D nyelvben a tiszta függvények:

Gyakorlati okokból azonban definiálhatnak lokális módosítható változókat és ezeket módosíthatják is. Foglalhatnak memóriát és dobhatnak kivételeket.

pure int f(int x, int y) { int z = x * y; if (z < 0) { throw new Exception("Rossz paraméter"); } return z; }