Język D – wieloparadygmatowy język programowania umożliwiający programowanie obiektowe, imperatywne oraz metaprogramowanie. Został opracowany przez Waltera Brighta, twórcę pierwszego natywnego kompilatora C++, Zortech C++. D powstał jako obiektowy następca języka C, jednak w przeciwieństwie do C++ zachowuje ze swoim poprzednikiem zaledwie binarną kompatybilność. D ma wiele cech obecnych w C++, a jego składnia oraz możliwości są wzorowane na Javie, C# oraz Eiffelpotrzebne źródło. Pierwsza stabilna wersja języka ukazała się 2 stycznia 2007 roku[1]. 17 czerwca 2007 roku opublikowano po raz pierwszy eksperymentalną wersję 2.0[2].
Możliwości
Język D jest projektowany bardziej z praktycznej, niż z teoretycznej perspektywy. Jego składnia oraz możliwości zostały opracowane na podstawie praktycznych wniosków z użytkowania oraz implementowania C++. Zrezygnowano z wstecznej kompatybilności kodu źródłowego na rzecz przejrzystej, bezkontekstowej gramatyki ułatwiającej wykonywanie kompilatorów, a także z poniektórych możliwości dostępnych w C++ takich, jak wielokrotne dziedziczenie na rzecz interfejsów oraz domieszkowania. W zamian wprowadzono wiele nowoczesnych rozwiązań: programowanie kontraktowe, prawdziwe moduły, wbudowane automatyczne zarządzanie pamięcią (przez garbage collector), testy jednostkowe, tablice asocjacyjne, tablice dynamiczne, domknięcia, funkcje anonimowe, funkcje zagnieżdżone, klasy wewnętrzne, leniwe wartościowanie oraz zintegrowany z kompilatorem system dokumentacji ddoc. nieoczekiwanie tego całkowicie przeprojektowano mechanizm szablonów. Utrzymana była możliwość tworzenia niskopoziomowego kodu oraz umieszczania wstawek asemblerowych.
Paradygmaty programowania
D wspiera trzy główne paradygmaty programowania: imperatywne, obiektowe oraz metaprogramowanie.
Programowanie imperatywne
Programowanie imperatywne wygląda prawie identycznie, jak w C. Dostępne są klasyczne pętle, instrukcje warunkowe, funkcje, zmienne lokalne oraz globalne oraz wyrażenia, które działają identycznie, jak w pierwowzorze, a programy posiadają bezpośredni dostęp do biblioteki standardowej C. Ponadto wprowadzone zostały dwa rozszerzenia w postaci instrukcji foreach do iterowania po kolekcjach oraz funkcji zagnieżdżonych, które posiadają dostęp do zmiennych lokalnych funkcji nadrzędnej.
Programowanie obiektowe
Klasy w D składają się na pojedynczą hierarchię wywodzącą się z klasy Object. Język nie wspiera wielokrotnego dziedziczenia, proponując w zamian interfejsy w stylu Javy oraz domieszki, które dopuszczają przeniesienie współdzielonej funkcjonalności poza hierarchię klas.
D zmienia także sposób obsługi metod wirtualnych. Domyślnie każda metoda, która nie jest statyczna, prywatna oraz szablonowa, staje się metodą wirtualną. Kompilator analizuje hierarchię klas, określając które metody wirtualne da się wywoływać statycznie oraz w jakim kontekście, co dopuszcza na lepszą optymalizację kodu wynikowego oraz uniknięcie wielu błędów programistycznych.
Metaprogramowanie
Metaprogramowanie jest wspierane przez kombinację szablonów, wykonywania funkcji w trakcie kompilacji, krotek oraz domieszkowania tekstu. Poniższe przykłady demonstrują pewne możliwości metaprogramowania w D.
W przeciwieństwie do C++, szablony w D bardziej przypominają funkcje. Poniższy przykład pokazuje wykorzystanie statycznego ifa, instrukcji wykonywanej w trakcie kompilacji, do obliczania wartości silni:
template Factorial(ulong n)
{
static if(n <= 1)
const Factorial = 1;
else
const Factorial = n * Factorial!(n - 1);
}
Poniżej pokazana jest natomiast klasyczna funkcja, która bardzo jest podobne wersję z szablonem:
ulong factorial(ulong n)
{
if(n <= 1)
return 1;
else
return n * factorial(n - 1);
}
Poniżej pokazane jest wykorzystanie szablonu oraz funkcji do obliczenia wartości silni w trakcie kompilacji. W języku D nie ma konieczności deklarowania typów stałych, albowiem potrafią one zostać wyliczone z prawej strony wyrażenia.
const fact_7 = Factorial!(7);
Zwykłe funkcje bywają obliczone w trakcie kompilacji, kiedy są użyte w stałych wyrażeniach spełniających określone kryteria.
const fact_9 = factorial(9);
Szablon std.metastrings.Format udostępnia formatowanie tekstu w stylu printf, którego wynik jest wyświetlany podczas kompilacji przez dyrektywę msg.
import std.metastrings;
pragma(msg, Format!("7! = %s", fact_7));
pragma(msg, Format!("9! = %s", fact_9));
Domieszkowanie tekstu w połączeniu z funkcjami wykonywanymi w trakcie kompilacji dopuszcza na dynamiczne generowanie kodu D podczas kompilowania programu. Może to być wykorzystane do parsowania języków dziedzinowych do kodu D, który następnie jest włączany jako cząstka powstającego programu D.
import FooToD; // hipotetyczny moduł z funkcją tłumaczącą język Foo na D
void main()
{
mixin(fooToD(import("example.foo")));
}
Zarządzanie pamięcią
Choć D jest kompilowany do kodu maszynowego, zarządzanie pamięcią domyślnie przebiega się za pośrednictwem odśmiecacza pamięci, identycznie jak w językach interpretowanych. Część obiektów bywa usuwana natychmiast po znalezieniu się poza zasięgiem. Pomimo tego, programista ma pełną kontrolę nad całym procesem. Dozwolone jest ręczne alokowanie oraz zwalnianie pamięci poprzez przeciążone operatory new oraz delete albo poprzez zwyczajne wywołanie funkcji malloc() oraz free() z biblioteki standardowej C. Możliwe jest zmienianie zakresów pamięci obserwowanych przez odśmiecacz, wstrzymywanie oraz wznawianie jego pracy, a także wymuszanie wykonania ogólnego albo pełnego czyszczenia. Dokumentacja podaje przykłady implementacji innych technik zarządzania pamięcią dla sytuacji, w których odśmiecanie nie jest optymalnym rozwiązaniem.
Łączenie z innymi językami
Język D jest binarnie kompatybilny z C, co dopuszcza łączyć napisane w nim programy ze wszystkimi bibliotekami C, w tym także z biblioteką standardową tego języka, która jest także częścią biblioteki standardowej D.
Łączenie z kodem C++ nie jest obsługiwane przez wersję 1.0. Eksperymentalna częściowa obsługa ukazała się w wersji 2.0.
D 2.0
W czerwcu 2007 roku ukazał się D 2.0, nowa eksperymentalna gałąź języka, która koncentruje się na jego dalszej rozbudowie. Pewne z nowych wprowadzanych przez nią cech to:
- Częściowa obsługa interfejsu binarnego C++.
- Iteracja
foreach po wybranym wycinku kolekcji.
- Obsługa prawdziwych domknięć. W wersji 1.0 domknięcia nie bywają bezpiecznie zwracane przez funkcje, albowiem skutkuje to utratą dostępu do danych zaalokowanych na stosie.
- Obsługa funkcji czystych, które nie posiadają efektów ubocznych: potrafią odwoływać się zaledwie do niezmiennych danych oraz wywoływać inne funkcje czyste. W połączeniu z prawdziwymi domknięciami daje to pełną obsługę programowania funkcyjnego.
- Funkcje
nothrow.
- Operacje wektorowe, np.
c[] = b[] + a[]; (sumowanie odpowiadających sobie elementów z dwóch tablic) albo b[] *= 3; (pomnożenie każdego elementu tablicy przez 3).
- Ulepszenia w bibliotece standardowej Phobos.
Implementacje
Obecne implementacje języka D generują kod maszynowy, aby zapewnić maksymalną wydajność.
Chociaż D cały czas się rozwija, do wersji 1.0 nie są już wprowadzane żadne zmiany, a cały wysiłek koncentruje się na usuwaniu wykrytych błędów. Oficjalny kompilator Waltera Brighta definiuje równocześnie sam język.
Aktualnie rozwijane są cztery kompilatory języka D:
- DMD - oficjalny kompilator Waltera Brighta. Front-end kompilatora objęty jest zarówno licencją artystyczną, jak oraz GNU GPL, a jego kod jest rozpowszechniany razem z binariami. Umożliwia to szybkie wykonywanie nowych kompilatorów. Dostępne są także źródła back-endu, lecz nie są one objęte żadną otwartą licencją. Kompilator wspiera ograniczoną liczbę systemów operacyjnych oraz platform sprzętowych (w szczególności brakuje obsługi architektury 64-bitowej).
- GDC - front-end dla kompilatora GCC wykorzystujący kod z DMD. Aktualnie jest uznawany za martwy projekt, jednak ostatnio podjęte zostały próby napisania nowego front-endu, który domyślnie wspierać ma eksperymentalną wersję 2.0 języka[3]
- LDC - front-end dla kompilatora LLVM wykorzystujący kod z DMD. Obsługuje zarówno wersję 1.0, jak oraz 2.0, a także bibliotekę Tango oraz platformy 64-bitowe.
- D Compiler for .NET - eksperymentalny port kompilatora na platformę .NET[4]
Narzędzia programistyczne
D wciąż nie jest wspierany przez wiele środowisk IDE, co bywa przeszkodą dla części użytkowników. Dostępne edytory zawierają w sobie m.in. Eclipse, SlickEdit, Entice, Designer, emacs, vim, SciTE, Smultron, TextMate, Zeus czy Geany. Vim wspiera zarówno kolorowanie składni, jak oraz uzupełnianie kodu. Dodatkowy pakiet dopuszcza na pracę w TextMate, a IDE Code::Blocks częściowo obsługuje język, jednak brakuje wciąż takich możliwości, jak uzupełnianie kodu oraz refaktoryzacja.
Istnieją przynajmniej dwie aktywnie rozwijane wtyczki dla języka D dla platformy Eclipse: Descent and Mmrnmhrm.
Ponadto, powstają otwarte środowiska napisane w całości w D (np. Poseidon), które posiadają wszystkie niezbędne opcje.
Aplikacje D bywają analizowane debugerami stworzonymi dla C/C++, np. GDB oraz WinDbg, lecz z ograniczoną obsługą bardziej zaawansowanych możliwości języka. Pełną obsługę oferuje debuger Ddbg w Windows oraz bywa używany z zewnętrznymi środowiskami programistycznymi albo za pomocą wiersza poleceń. Komercyjny ZeroBUGS dla Linuksa eksperymentalnie obsługuje D oraz ma własny interfejs użytkownika.
Problemy oraz kontrowersje
Dwie biblioteki standardowe
Biblioteka standardowa D nosi nazwę Phobos. Część programistów ze społeczności D uważa, że jest ona zbyt uproszczona oraz ma wiele nieścisłości, dlatego rozpoczęli prace nad alternatywną biblioteką Tango. W D 1.0 obie biblioteki są ze sobą wzajemnie niekompatybilne, albowiem używają innych odśmiecaczy pamięci, mechanizmów wielowątkowości itd. Obecność dwóch równie popularnych bibliotek prowadzi do wielu problemów z przenośnością: cząstka pakietów wykorzystuje z Tango, inne z Phobosa.
Problem jest w dużej mierze rozwiązany w D 2.0, w którym powstało jednolite środowisko uruchomieniowe druntime, na które obie biblioteki zostaną przeportowane, dzięki czemu stanie się możliwe ich jednoczesne wykorzystanie. Aktualnie (styczeń 2010) Phobos zakończył już proces migracji. W dającej się przewidzieć przyszłości język będzie posiadać dwie konkurujące biblioteki standardowe, lecz od wersji 2.0 będą one ze sobą kompatybilne na podstawowym poziomie, oraz współdzielić pewne podstawowe funkcje (np. interfejs tablic asocjacyjnych czy interfejs zarządzania pamięcią).
Nieukończona obsługa bibliotek współdzielonych
Problem obsługi bibliotek współdzielonych związany jest z działaniem kompilatorów, nie z samym językiem.
Kompilator DMD ma problemy z tworzeniem bibliotek współdzielonych na systemach Unix. Uniksowy format ELF jest wspierany jako rozszerzenie kompilatora GDC. W systemach Windows biblioteki DLL są poprawnie obsługiwane oraz możliwe jest przekazywanie obiektów kontrolowanych przez odśmiecacz pamięci do funkcji C. Są jednak pewne ograniczenia związane z użyciem obiektów. Związane jest to z mechanizmem automatycznego zarządzania pamięcią, który stosuje D.
W środowisku D istnieje tendencja do kompilowania statycznie całego programu, wraz z optymalizacją globalną oraz międzyproceduralną, co w przypadku bibliotek współdzielonych jest wysoce utrudnione. Kompilator DMD działa bardzo szybkopotrzebne źródło, dlatego równoczesne kompilowanie wielu plików w jednym przebiegu nie stanowi istotnego problemu.
Przykładowy program
Prosty program wypisujący argumenty, z jakimi stał się wywołany:
import std.stdio;
int main(char args)
{
foreach(i, a; args)
writefln("args[%d] = '%s'", i, a);
return 0;
}
Linki zewnętrzne
Przypisy