Jeden język by zawładnąć wszystkimi
Osvaldo Pinali
Doederlein
tłumaczenie na język polski (z paroma komentarzami): Jarosław
Staniek
[wersja do wydruku w formacie pdf]
oryginał artykułu: One Runtime to
Bind Them All
Kwiecień 2002
Spis treści
- 1 Wprowadzenie
- 2 Jeden język by zawładnąć wszystkimi
- 3 Załączniki: Dyskusje, poprawki, objaśnienia
- 4 Wyjaśnienia trudniejszych terminów
1 Wprowadzenie
Oryginalny artykuł ,,Jeden język by zawładnąć wszystkimi'' wywołał duży odzew u ludzi reprezentujących różne stanowiska i poglądy. Co ważne, wypowiedziały się także osoby posiadające większą wiedzę na niektóre z poruszanych przeze mnie zagadnień. Zdecydowałem się nie korygować oryginalnego dokumentu, lecz zamiast tego napisać erratę. Z erraty tej powstał załącznik, z załącznika zaś, nowy artykuł.
Czytając cały dokument w starej oraz nowej wersji, łatwo przekonamy się, że większość moich opinii nie uległa istotnym zmianom. Oczywiście, w niektórych zagadnieniach co najmniej częściowo widać moją prywatną opinię (tak, przyznaję się, że niekiedy reprezentuję postawę ,,purystów językowych'' oraz tych z nas, którzy mają złe odczucia związane z samą strategią firmy Microsoft, nawet w oderwaniu od jej technologii). W przypadkach, gdy nie zdołałem wesprzeć mojego stanowiska bezdyskusyjnymi, ściśle technicznymi argumentami, starałem się przedstawić czytelnikowi oba punkty widzenia, wsparte odpowiednią konkluzją. Aby to zrobić włączyłem do tekstu wiele cytatów pochodzących z mojej korespondencji z ekspertami w danej dziedzinie. Warto wspomnieć, że było to trudne zadania: w kilku przypadkach, krytyczny czytelnik może pomyśleć, że manipulowałem komentarzami (np. zmieniając ich kontekst). Gdybym jednak załączył wszystkie e-maile, artykuł stałby się o wiele dłuższy i trudniejszy w odbiorze.
Problem został w dużym stopniu rozwiązany przez Erika Meijer'a, menadżera programu w projekcie CLR firmy Microsoft. Szczegółowo rozwinął on zarówno informacje pochodzące z oryginalnej wersji artykułu jak i bieżących załączników. Zdecydowałem się na proste przeniesienie komentarzy Erika do przypisów, oznaczając je dodatkowo symbolem [EM].1 W ten sposób czytelnik łatwo odróżni opinię adwokatów Javy od opinii Microsoft'u. Mam nadzieję, że jest to uczciwe podejście, ponieważ w niniejszym tekście jest mowa o technologii firmy Microsoft. Liczę także na pewne walory edukacyjne tekstu. Jeśli już czytałeś oryginalną wersję artykułu, zapewne wystarczy, abyś zaznajomił się tylko z przypisami.
Przypisy
- ...].1
- [JS] Przypisy autora artykułu oznaczono przez [OPD]. Przypisy tłumaczącego oznaczono przez [JS]. Odnośniki do wyjaśnienia trudniejszych terminów mają postać [obj*].
2 Jeden język by zawładnąć wszystkimi
Bez wątpienia platforma .NET jest ogromnym krokiem na przód, jeśli porównamy ją z poprzednimi zestawami dla programistów (SDK) Microsoftu. Mimo tego widać jak duża odległość dzieli tzw. ,,wizję marketingową'' od realnego świata. Podobnie jak wcześniejsze technologie Win32, MFC, COM itd., .NET będzie starał się konkurować z istniejącymi platformami programistycznymi (takimi jak Java) o wiodącą pozycję w dziedzinie rozwoju oprogramowania następnej generacji. Microsoft jawnie wyznaczył sobie technologię Java 2 Enterprise Edition (J2EE) jako ,,wroga nr 1'', przeznaczonego do pokonania. Porównania .NET z technologią Java dopiero od niedawna wyskakiwały niczym królik z kapelusza w rozmaitych marketingowych artykułach Microsoftu lub innych sponsorowanych przez niego firm. Takie nagłe skoncentrowanie się firmy Microsoft na opisywaniu i osądzaniu technologii Java jest co najmniej dziwne, jako że aż do teraz Microsoft próbował kompletnie ignorować istnienie Javy. Dokumenty, choćby na temat języka C#, wspominały jedynie o jego związkach z językami C i C++, nie odnosząc się do faktu, że C# jest niemal wierną kopią Javy z pewnymi szczególnymi zmianami.
Jednym z istotnych tematów w dyskusji nad CLR (Common Language Runtime) jest jego neutralność językowa (language-neutral nature). Mówiąc w skrócie, CLR jest konkurentem maszyny wirtualnej Javy (JVM, Java Virtual Machine), działającym w .NET. CLR jest maszyną wirtualną, która pozwala na uruchamianie przenośnego kodu bajtowego (bytecode), znanego też pod nazwą MSIL (Microsoft Intermediary Language). Rzekomo, CLR ma być równym stopniu przyjazny dla dowolnego języka programowania, jaki zechcielibyśmy zastosować w .NET. W ten sposób, ta jego domniemana zaleta ma być podstawowym bodźcem do stosowania platformy .NET. Wspomniana cecha jest rozgłaszana szeroko jako ,,wolny wybór języka programowania''. Mówi się, że CLR może nie tylko wspierać wiele języków - ma on także ułatwiać ich łączenie: w jednym programie moglibyśmy stosować klasy napisane w wielu różnych językach. Wśród możliwości znaleźlibyśmy także tzw. miedzy-językowe dziedziczenie (cross-language inheritance), np. klasa w C# dziedziczy z klasy Visual Basica. Implementacja CLR została wsparta dwoma związanymi z nią specyfikacjami: CLS (Common Language Specification) oraz CTS (Common Type System), które języki od innych dostawców powinny spełniać, aby można było kompilować ich kod dla CLR.
2.1 Typowe pułapki
Krytyka między-językowych cech systemów programowania jest przede wszystkim związana z ich aktualną szkodliwością dla procesu rozwoju oprogramowania. Jest to łatwe do zrozumienia zwłaszcza dla architekta oprogramowania czy lidera projektu. Nawet w przypadku projektów opartych o pojedynczy język oraz zestaw komponentów (np. C++/MFC lub Java/J2EE) oraz dodatkowe narzędzia (środowiska IDE, CASE, kontroli wersji), nierzadko wiele trudu wymaga połączenie tego wszystkiego tak, by praca przebiegała płynnie. Systemy wspomagające rozwijanie projektów programistycznych nigdy nie są perfekcyjnie wykonane i udokumentowane. Ludzie w każdym rzeczywistym zespole mają różne umiejętności, doświadczenie czy choćby gust w odniesieniu do projektowania i programowania. Każde powiększenie liczby programistów w projekcie oznacza konieczność poniesienia znacznie większych kosztów w celu integrowania wyników ich pracy, testowania i zarządzania. Komponenty informatyczne wykonane przez większe zespoły zazwyczaj też znacznie mniej nadają się do powtórnego wykorzystania w przyszłości (reuse). Kiedykolwiek programista A nie rozumie (albo nie lubi) kodu napisanego przez programistę B, wynikiem może być zmarnowanie części pracy, praca nadmiarowa oraz nowe błędy.
Powyższa krytyka w powszechnej opinii nie zawsze jest typowa. Zauważmy jednak, iż oczywiście nikt nie lubi dziedziczyć tysięcy linii kodu napisanego w jakimś dziwacznym, niezrozumiałym języku. Nie mówiąc już o kodzie napisanym przez osobę, która np. jakiś czas temu odeszła z zespołu i nie ma z nią kontaktu. Zazwyczaj menadżerowie projektów nie są skłonni pozwalać każdemu swojemu człowiekowi na używanie jego ulubionych, być może niepospolitych narzędzi (wyjątkiem są badania nad przydatnością nowych narzędzi). Z drugiej strony sami menadżerowie i architekci forsują w projekcie zwyczaj używania preferowanych przez siebie technologii, nie zawsze licząc się z długo terminową kalkulacją kosztów ich decyzji. Jednak jak dotąd, nawet to ryzyko może być kontrolowane. Jest to zwykle kwestia wyboru i odpowiedzialności.
W naszej dyskusji nie możemy zwracać się przeciwko możliwości wyboru wielu języków programowania dla projektu, ponieważ wiele projektów odniosło i wciąż odnosi korzyści wynikające z tej możliwości. Tu jednak, aby być uczciwym, należy stwierdzić jeden fakt. Łączenie ze sobą konstrukcji wykonanych w różnych językach programowania nie przebiega tak gładko, jak to obiecuje .NET. Zazwyczaj jest tak, że do łączenia kodu różnych języków w jeden program potrzebujemy pewnego rodzaju ,,mostów'', zwanych czasem łącznikami międzyjęzykowymi (cross-language bindings). Wiele osób widzi te bariery jako coś pozytywnego:2 zapewniają one minimalizację interfejsów, sprzyjają stosowaniu technik obiektowych oraz możliwości ponownego użycia komponentów na zasadzie ,,czarnych skrzynek''. Rzecz jasna, korzyści jakie odnosimy w ten sposób zależą od dyscypliny w zespole programistycznym.
Paradoksalnie, cała dyskusja w tym rozdziale doprowadziła nas do mniej lub bardziej podświadomego wniosku, że .NET jest platformą językowo-neutralną, co jest po prostu nieprawdą (w każdym razie nie w takim stopniu jak rozgłasza to Microsoft). Co najwyżej, można zaryzykować stwierdzenie, że .NET ma cechy ,,częściowo językowo-neutralnego'' środowiska. Ponieważ aspekty tej ,,częściowości'' są bardzo ważną sprawą, zapoznamy się z nimi dokładniej.
Przypisy
- ... pozytywnego:2
- [EM] Tutaj nieco sobie zaprzeczasz. Z jednej strony piszesz, że wielość
języków jest dobra, z drugiej strony twierdzisz, że kłopotliwość wykonywania
łączeń między językami jest pozytywna. Nie specjalnie podoba mi się ten ostatni
argument. Profesor Kees Koster, mój promotor, zawsze mówił, że języki programowania
powinny ułatwiać wykorzystywanie ich w poprawny sposób, a nie utrudniać niepoprawnego
ich wykorzystywania.
[OPD] Współpraca ze sobą wielu języków jest rzeczą wspaniałą. Postawę, którą krytykuję określiłbym raczej mianem ,,nadmiernej swobody języka''. Chodziło mi o taki stan, gdy dobre cechy jednego języka są niszczone w imię jego zgodności z innymi językami. Argument o nie utrudnianiu robienia niepoprawnych rzeczy jest całkiem dobry, jednak wygląda na to, że większość użytkowników Javy wierzy, że powinno się maksymalnie utrudnić co najmniej najniebezpieczniejsze skłonności obecne wśród programistów (np. niebezpieczne wskaźniki, ręczna alokacja pamięci), tak jak to jest np. w ,,C+JNI''.
2.2 Jak ,,ogólny'' jest CLS?
Platforma .NET próbuje być językowo neutralna. Jest to prawdą dla wielu języków podobnych do C#, których semantyka, system typowania i reguły obowiązujące w czasie wykonania są odrobinę bogatsze od Javy lecz wciąż ubogie z punktu widzenia wielu innych języków. Co więcej, język C# jest określony przez Microsoft jako relacja 1-do-1 języka w stosunku do cech opisanych przez CLR/CTS/CLS (dokładnie tak samo, jak ma się Java do JVM).3
Różnorodność języków programowania jest spowodowana nie tylko istnieniem wielu rozmaitych zadań do jakich chcemy ich używać (od programowania systemowego aż do sztucznej inteligencji) i które to zadania wymagają odmiennych narzędzi, ale również tym, że po prostu nie istnieje ,,Jeden Właściwy Sposób'' na rozwiązanie tych zadań. Po pół wieku badań, informatycy muszą być są w stanie jednoznacznie odpowiedzieć na większość pytań dotyczących projektowania języków programowania.
Znakomita większość zespołów programistycznych spędza swoje produktywne życie,
używając dosłownie paru najpopularniejszych języków, traktując wiele unowocześnień
i rozszerzeń jako akademickie zabawki, które zapewne nigdy nie trafią do użytku.
Okazuje się, że jest to fałszywe złudzenie: wystarczy uświadomić sobie, że dzisiejsze
cechy właśnie tych ,,najpopularniejszych'' języków jest efektem przeprowadzanych
dwadzieścia lat temu badań nad ,,akademickimi zabawkami''. Obiekty, odśmiecacze,
systemy rozproszone, maszyny wirtualne i wiele innych elementów współczesnej
technologii zapewne nie były wynalezione tydzień temu przez firmę Sun lub Microsoft.
Bez względu na to, co nam sugerują działy marketingu tych firm.4
Lista ważniejszych ograniczeń CLR/CTS/CLS (i w związku z tym
.NET).
Dziedziczenie implementacji. CLS umożliwia jedynie pojedyncze, statyczne dziedziczenie.
Języki takie jak C++ czy Eiffel potrzebują wielodziedziczenia implementacji (multiple inheritance of implementation, MI). Wsparcie między-językowowści dla MI może nie być możliwe, ponieważ MI stwarza kilka poważnych problemów (np. powtórzone dziedziczenie tych samych składowych czy konflikty nazw). Różne języki rozwiązują te problemy w sobie właściwy sposób, często zupełnie niekompatybilny między sobą.
Inną alternatywą dla MI jest tzw. konstrukcja z klasami mieszanymi, mix-ins [obj2], uważna przez wielu za o wiele lepsze rozwiązanie problemu, udostępniające dynamiczne, luźne powiązania sprzyjające powtórnemu użyciu (reuse). Rozwiązanie to nie jest w żaden sposób wspierane przez CLR, a jest wymagane przez takie języki, jak np. Python.5
Wynik wprowadzenia .NET: powstanie dialektów oryginalnych języków po ty, by stały się kompatybilne z CLR. Przykładami są okrojony C++ (Managed C++) i Eiffel# - oba nie pozwalające na wielodziedziczenie.
Typy parametryzowane (generic types). Obecnie w CLS nie ma żadnego wsparcia dla programowania generycznego (generic programming, [obj3]).
Obsługiwane są mechanizmy stosowane w czasie kompilacji, np. podobne do szablonów w C++ (templates), jednak nie są one przenośne między językami. Dla przykładu, nie ma możliwości utworzyć obiektu klasy stos<T> w języku C#. Instancje klas projektowanych z użyciem szablonów są dostępne z poziomu różnych języków z .NET, lecz ich definicje nie są wymienne. W C++, jeśli szablon klasy stos<T> skonkretyzujemy do stos<Pies>, metody takie jak stos<Pies>::dodaj(Pies) zostaną wygenerowane zgodnie z regułami opisanymi językiem C++ (np. reguły dopasowywania nazw). Te reguły nie są jednak częścią CLS. Czyni to niemożliwym dla programu ,,widzieć'' wspomnianą klasę stos<Pies> jako STOS[PIES] w języku Eiffel. Będzie tak, dopóki w tym drugim języku nie zaakceptuje się reguł C++ jako standardu ,,de facto''. Inny podejściem jest stosowanie ,,erasure model'' [obj4], użytego np. w ogólnej Javie.6Podsumowując, typy parametryzowane nie oznaczają wszędzie tego samego. Ustanowienie między-językowego standardu może okazać się po prostu niepraktyczne.
Wynik wprowadzenia .NET: Niepełna obsługa typów parametryzowanych bez wsparcia dla między-językowości. Konieczność rezygnacji z ważnych szczególnych cech danej implementacji, jak np. covariance [obj5] pochodzącej z Eiffel#. Wymuszenie poprzez biblioteki systemowe używania niejedoznacznych, niedospecyfikowanych referencji do obiektów Object.
Statyczne typowanie.7CLS oferuje mieszankę typów podstawowych, strukturalnych oraz obiektów wymagając statycznej deklaracji dającej co najmniej elementarną informację o typie. (Specyficzne typy obiektów mogą być wykrywane w czasie wykonania lecz system musi być (statycznie) poinformowany, że konkretne słowo w pamięci jest wskaźnikiem na obiekt, liczbą całkowitą, zmiennoprzecinkową albo czymkolwiek innym mającym typ postawowy).
Nie jest to dobre dla języków stosujących całkowicie dynamiczne typowanie w celu uniknięcia złożoności systemów z typowaniem statycznym. Przykładami takich języków jest Smalltalk, LISP oraz ich pochodne. Przechowywanie w pamięci najbardziej podstawowych wartości takich jak całkowite, zmiennoprzecinkowe czy logiczne jako w pełni opisane, dynamicznie typowane encje, jest zawsze kosztowne. Aby się przed tym uchronić, systemy wykonawcze języków na dodatkowe informacje przeznaczają tzw. znaczniki w pamięci: parę bitów każdego słowa pamięci należącego do danej wartości służy do przechowywania identyfikatora typu. W ten sposób dość tanio można, choćby zgrubnie, zidentyfikować najważniejsze typy obiektów w systemie. (Typowym efektem ubocznym stosowania takiej reprezentacji są nierówne, nietypowe zakresy poszczególnych typów, np. typ całkowity 4-bajtowy może mieć 30 bitów zamiast 32, ponieważ dwa bity pochłonął znacznik typu).
Techniki używane w takich systemach są odmienne od tradycyjnych. Głównym problemem jest to, że jakikolwiek wysoko wydajny odśmiecacz (GC, Garbage Collector) wymaga precyzyjnej identyfikacji wskaźników. W systemach typowania statycznego, kompilator JIT (Just In Time) generuje ,,tablice typów'' (type maps) dla każdej klasy i metody. GC uzyskuje z tych tablic informację o tym, które pola, pozycje stosu lub rejestry są wskaźnikami. Sposób ten nie jest wystarczająco dobry dla języków z dynamicznym typowaniem, gdzie GC, aby odróżnić obiekty od typów podstawowych, kieruje się wspomnianymi wyżej znacznikami bitowymi. CLR, aby poprawnie obsługiwać obie grupy języków, musiałby realizować oba sposoby przechowywania informacji o typach. Na tej samej stercie musiałyby być przechowywane zarówno wartości o typach podstawowych zawierające i niezawierające znaczniki bitowe. Do tego dochodzi konieczność przechowywania takich samych informacji o referencjach określonych typów. Na domiar złego, różne języki z dynamicznym typowaniem mogą stosować różne metody i formaty zapisu znaczników bitowych w pamięci.
Metody. Wiemy już, że CLS ma kilka istotnych niedomagań, jeśli chodzi o modele dziedziczenia. Patrząc zaś na obsługę metod, jest już o wiele gorzej. CLS obsługuje jedynie tradycyjny model oparty na tablicy metod wirtualnych (vtable). Obsługiwane są jedynie wywołania dla pojedynczego odbiorcy (single-receiver dispatch, [obj8]), statycznie typowane sygnatury oraz pojedynczy powrót z metody. Nie ma także metod ,,first-class'' [obj9].
Co więcej, w świecie języków nie ma powszechnej zgody jak należy definiować i wywoływać metody. W językach z dynamicznym typowaniem nie ma sposobu na automatyczne typowanie sygnatur dla metod. Kompilator może zadeklarować cokolwiek jako ,,Object'', lecz użytkownicy innych języków mogą tego nie preferować.
Część języków obsługuje tzw. wielo-metody (multi-methods, [obj10]), gdzie wywołanie jest polimorficzne na kilku argumentach (a nie tylko jednym, odpowiadającym odbiorcy - zazwyczaj this lub self). Mając np. klasy Liczba i Macierz oraz cztery wersje metody pomnóż(lewy,prawy), dzięki sprawdzeniu typów przekazywanych przy wywołaniu argumentów, w czasie kompilacji zostanie wybrana właściwa wersja metody pomnóż. (W wielu językach przeciążanie emuluje to zachowanie, ale ponieważ wtedy wołanie jest statyczne, nie działa to dobrze z parametrami przekazywanymi przez referencję. Wywołania wielo-metod są dynamiczne i w pełni obsługują dziedziczenie i polimorfizm).
Stosowanie tablic metod wirtualnych (Vtables, czyli tablic wskaźników do kodu metod) nie nadaje się w przypadku wielu języków. Ten statyczny mechanizm jest dobry, gdy znany jest statyczny typ danych na których operujemy przy wywołaniu metody. Musi to być choćby informacja o obiekcie, którego dotyczy wołanie. Jeśli np. wywołamy ,,obiekt1.zróbCoś()'' i metoda ,,zróbCoś()'' jest zdefiniowana dla typu X, musimy przynajmniej wiedzieć o tym, że obiekt1 jest typu X. Jeśli brakuje nam tej informacji, niezbędne są jakieś alternatywne sposoby wywołania. Metoda tworzenia odbić (reflection, [obj11]) działa, lecz nieefektywnie. Języki dynamiczne stosują często model, gdzie wołanie metod opiera się na selektorze (wbudowanej sygnaturze). Dynamiczny typ odbiorcy wołania jest zestawiany z selektorem i na tej podstawie w tablicy haszującej obsługiwanej przez maszynę wirtualną można efektywnie zlokalizować odpowiednią metodę.
Kończąc temat metod, należy dodać, że CLR nie umożliwia stosowania bloków kodu (full closures, blocks) a jedynie wskaźniki do metod.8Wskaźnik taki zawiera jednocześnie w pojedynczej zmiennej adres metody i odbiorcy. Jest wystarczający np. do realizacji obsługi zdarzeń. Bloki kodu idą o krok dalej. Są one powiązane z wszystkimi zmiennymi widocznymi w ich zasięgu statycznym (być może innym bloku). Umożliwa to stosowanie wielu nowoczesnych technik programistycznych (jeśli chcesz uszczęśliwić zwolennika Smalltalka, poproś go o przykłady). Podobnie jak to jest z wewnętrznymi klasami w Javie, w C# (oraz CLR) przyjęto prymitywną alternatywę dla bloków, zasłaniając się kłopotami z wydajnością.
Zagadnienia wydajnościowe. System wykonawczy jest optymalizowany wg ściśle określonych założeń.9Działanie każdego kompilatora JIT, odśmiecacza itp. opiera się na pewnych uproszczeniach i założeniu, że będziemy mieć do czynienia z oczekiwanymi, przewidzianymi sytuacjami.
W przypadku CLR, zakładamy, że wszystko (języki, maszyna wykonawcza) jest zoptymalizowane dla C#. Rezultatem poczynienia takiego założenia jest poważnie mniejsza efektywność działania jakiegokolwiek języka istotnie różnego od C#.
Jako prosty przykład możemy wziąć zarządzanie pamięcią. Programy w C# nie generują dla CLR zbyt dużego obciążenia pamięci (patrząc na liczbę alokacji i zwolnień), gdyż korzystają z ręcznej alokacji na stercie i semantyki opartej na wartościach (value semantics, [obj12]). Przez to mniej intensywnie korzystają one ze sterty niż, np. Java czy Smalltalk. Programy pisane w innych (niż C#) językach będą w ten sposób działać mniej efektywnie niż ich ,,natywne'' odpowiedniki (czyli np. Java pod JVM). Zauważmy, że maszyny wirtualne zaprojektowane nawet tylko dla pojedynczego języka (np. Java) zazwyczaj posiadają wiele implementacji i komponentów do wyboru, takich jak różne odśmiecacze czy kompilatory JIT. Po prostu nie istnieje jedno idealne rozwiązanie dla każdego zastosowania.
Od czasu, gdy CLR obsługuje wiele różnych języków, ludzie zaczęli pisać odpowiednie mikro-benchmarki w tych językach i uruchamiać je przy pomocy CLR, aby dokonać niezbędnych testów. Język C#, co oczywiste, działa pod CLR szybciej niż jakikolwiek inny. Stąd niektórzy całkowicie błędnie przyjmują, że C# jest językiem o największej bezwzględnej szybkości wykonania (tzn. nie uwzględniając narzutu VM) spośród obecnie dostępnych. Tymczasem w rzeczywistości parametr ten nie ma istotnego znaczenia przy porównywaniu języków. Kluczowym czynnikiem, który powoduje różnicę w efektywności działania języków pod CLR jest system typowania. Każdy język, którego typy podstawowe nie ,,rzutują się'' bezpośrednio na typy w CTS, potrzebuje pewnej porcji dodatkowego kodu wstawianego przez kompilator. Kod ten we wszystkich punktach programu, w których korzysta z innych języków, dokonuje konwersji typów do postaci akceptowanej przez CLR. Dotyczy to także wszystkich metod publicznych zapisanych w tych językach.10
Innym interesującym aspektem jest stos wywołań. Większość języków przechowuje na nim informacje o wywołaniach metod w postaci ramek, które są zdejmowane z niego podczas powrotu z metody lub zaistnienia wyjątku. Niektóre języki stosują inne podejście, zwane kontynuacjami (continuations, [obj13]),11 wyszukany mechanizm, przy użyciu którego metody w rzeczywistości nigdy nie powracają do punktu wołania, lecz ,,kontynuują'' wykonanie w nowym kontekście programu. Działa to tak, jakby reszta programu (za miejscem wołania) została wstawiona w miejscu punktu powrotnego. Implementacja tego mechanizmu jest zupełnie inna od typowej, gdyż ramki kontynuacji w nie są umieszczane na stosie, lecz na stercie. Opisany model ma ogromny wpływ na metody optymalizacji kompilatora, jednak CLR nie umożliwia jego użycia. W efekcie języki oparte na kontynuacjach (np. Python) mogą co prawda być obsługiwane z poziomu CLR, lecz kosztem jest znaczny spadek efektywności oraz cech wielo-językowości.
Dodatkowe uwagi. CLS określa wiele zagadnień, które nie są językowo neutralne.
Przykład dla składni: identyfikatory. CLS jednoznacznie definiuje, że identyfikatorów nie można rozróżniać po wielkości liter. Jest to zachowanie niezgodne z większością języków uwzględniających wielkość liter.
Aby było ciekawiej, CLR obsługuje przeciążanie, co wymaga pewnych dodatkowych czynności (mapowania nazw) w przypadku języków nie oferujących takiej możliwości.12
Przykład dla systemu wykonawczego: Niektóre języki pozwalają obiektom lub klasom na zmianę ich typu lub struktury w dowolnym czasie wykonania programu.13Nie jest to wogóle przewidziane w CLR.
Przykład dla modelu programowania obiektowego: Nie wszystkie języki obiektowo zorientowane (OOL) są oparte o klasy. W językach opartycz o prototypy (Self, Javascript14) nie ma klas; są tylko obiekty (jednak każdy obiekt ma swą klasę). Nowy obiekt definiujemy przez sklonowanie istniejącego obiektu i dodanie do niego nowych atrybutów i metod.
W niektórych przypadkach istnieje możliwość zmodyfikowania języka tak, by stał się zgodny z CLS i jednocześnie nie został pozbawiony swoich charakterystycznych cech. Kiedy indziej może się zdarzyć, że najbardziej zaawansowane cechy danego języka nie są zbyt często stosowane. Pewne cechy mogą mieć zastosowanie np. tylko jako wsparci pracy dodatkowych narzędzi programistycznych stowarzyszonych z językiem (generatory kodu, debuggery). Wtedy alternatywne, zubożone implementacje dla platformy .NET są możliwe. Zawsze jednak istnieją scenariusze wzięte z realnego świata, gdzie zaawansowane cechy języków są absolutnie wymagane i nie można z nich zrezygnować. Dobrym przykładem są mix-ins i prototypy: w obu przypadkach wymagana jest nie tylko dostępność operacji konstrukcji i mutacji klas, ale też ich efektywność.
Konstrukcje.15 Ostatnia (lecz nie najmniej ważna) kwestia: kiedy budujesz coś dla .NET i potrzebujesz, np. używać kolekcji, nie możesz stosować kolekcji wbudowanych w język Smalltalk lub powszechnie akceptowalnych kolekcji pochodzących z STL dla C++. Niestety, nie użyjesz żadnej konstrukcji dostarczanej i zintegrowanej z Twoim ulubionym językiem. Musisz stosować ,,zewnętrzne'' kolekcje platformy .NET.
Jeśli programujesz w Smalltalku, musisz zrezygnować z wywoływania metod, które biorą jako argumenty bloki kodu i aplikują ten kod dowolnym obiektom. Jeśli programujesz w C++, nie możesz robić wszelkich użytecznych rzeczy z szablonami. Jeśli programujesz w LISPie, nie zobaczysz atomów związanych w bożonarodzeniowe drzewko. Jeśli programujesz w języku Eiffel, nie odziedziczysz w klasie wielu implementacji, itd. Jakiekolwiek języki stosujesz w projekcie, każdy z nich musisz używać tak jak używa się C#, przynajmniej w każdym punkcie komunikującym się ze światem zewnętrznym (czyli ze wszystkim co nie jest napisane w Twoim języku).
Zdobyliśmy trochę doświadczeń z tym fenomenem, pracując nad rozproszonymi usługami (CORBA, COM) oraz usługami internetowymi (Web Services), jednak oferowały one zwykle proste, przejrzyste interfejsy i w większości przypadków można było stosować natywne konstrukcje języka ze wszystkimi ich cechami. Dla przykładu, programista Javy użyje standardowych kolekcji z tego języka do zaimplementowania struktur danych, natomiast sekwencji ze standardu CORBA tylko w przypadku komunikacji zdalnej. Aby napisać program działający zgodnie z regułami świata .NET, jesteś zobowiązany używać nie tylko jego własnych bibliotek dla kolekcji, ale też dla GUI, strumieni, operacji sieciowych, wątków itd.
Opisana zależność nie jest obca w środowisku JVM z udostępniającym API J2SE / J2EE. Interfejsy te z natury nie mogą być obce programistom, stosującym właśnie język Java, co oznacza dobre dopasowanie bibliotek do języka. Dokładnie analogiczne korzyści otrzymuje się z korzystania z języka C#, dla którego biblioteki .NET zostały zaadoptowane jako konstrukcja natywna. Należy pamiętać o tym, że programy pisane dla .NET w innych językach będą musiały mieć więcej kodu i będą bardziej złożone niż ich odpowiedniki w C#. Szczególnie, jeśli będą musiały zawierać swoje natywne biblioteki (Microsoft nazwałby je zapewne ,,legacy'') oraz konwertować typy danych w obustronnej komunikacji z .NET.
Do wyboru mamy więc: (a) konstrukcje związane ściśle z językiem nie będącym klonem C# albo (b) programowanie wielo-językowe z wszystkimi jego ograniczeniami. Nie można jednak wybrać obu możliwości.
Przypisy
- ... JVM).3
- [EM] Każda implementacja języka musi redukować lukę między samym językiem
a stworzoną dla niego maszyną wykonawczą (runtime). W pewnych przypadkach ta
luka jest mniejsza, w innych większa. Luka semantyczna między CLR a C# nie
jest duża; analogicznie jak między Javą a JVM. Jednak nie powiedziałbym, że
relacja między językiem a maszyną wykonawczą to 1-do-1. Dla przykładu: kompilator
Javy musi dodatkowo kompilować klasy zdefiniowane jako wewnętrzne (inner
classes, [obj1]), ponieważ JVM nie wspiera ich bezpośrednio (1-do-1). Na
odwrót - w CLR są cechy, do których nie masz dostępu z poziomu C#, np. filtrowanie
wyjątków czy hermetyczne klasy abstrakcyjne.
[OPD] W istocie, wiele użytkowników Javy często narzeka na to, że Sun hackuje bibliotekę i kompilator by otrzymać nowe cechy języka. Jest to nieuniknione ze względów, że technologia ciągle się rozwija oraz wymagana jest kompatybilność wersji. Myśle, że Sun jest w stanie wnieść poprawki do głównych specyfikacji tak, by nie było konieczne stosowanie hackerskich sztuczek po to, aby język mógł się rozwijać. Na pewno będzie to miało miejsce w ważniejszych uaktualnieniach.
- ... firm.4
- [EM] Nie sądzę, żeby goście od marketingu wmawiali, że te rzeczy są najnowszymi
wynalazkami. Jeśli czytałeś mój dokument - napisałem o tym, że przenośne i uniwersalne
środowiska wykonywania programów były przedmiotem badać od dawna.
[OPD] Zgadzam się całkowicie. To może być nawet jednym z powodów, dla których podchodzę sceptycznie do takich technologii: były one wyzwaniem przez bardzo długi czas, a rozwiązanie jak dotąd się nie pojawiło. Być może więc pełne rozwiązanie nie istnieje. Przypomnijmy sobie, od jak dawna ludzie szukają rozwiązania problemu wnioskowania typów, aby pokonać problemy wydajnościowe dotyczące języków z dynamicznym typowaniem.
- ... Python.5
- [EM] Jak powiedziałeś, wszystkie te języki stosują trochę różne sposoby
wielodziedziczenia. Problem w tym, który ze sposobów wybrać? W swojeje książce,
John Gough pokazuje sposób samodzielnej implementacji wielodziedziczenia używając
do tego wskaźników na funkcje. Ludzie od języka Eiffel także zaimplementowali
wielodziedziczenie. Poprosiłem ich, aby udostępnili szczegóły techniczne. W
każdym razie, aspekty wielojęzykowości w .NET zostaną opublikowane (aż w trzech
częściach, bo to dużo materiałów). W przyszłości będzie to jeden z rozdziałów
książki. Artykuł będzie opisywał m. in. aktualny stan działania języka Eiffel
w .NET. Ludzie od Eiffel'a są też w trakcie wnoszenia poprawek do artykułu o
tym języku w MSDN.
[JS] Jasne, że w większości języków o rozsądnej sile wyrazu można zasymulować różne zaawansowane konstrukcje. W C można symulować obiektowość, umawiając się np. które funkcje są metodami wirtualnymi i które uznajemy za prywatne. Sądzę jednak, że aby wykonywać takie sztuczki, nie musimy stosować technologii takich jak .NET (np. biblioteki projektu Gnome, www.gnome.org). Niestety, .NET nie rozwiązując problemu wielodziedziczenia, wprowadza dodatkową warstwę pośredniczącą, która należy się zajmować, zamiast skupiać się na właściwym zadaniu. Jeśli mamy projekt rozwijany od dawna w C++, gdzie używano tak standardowych jego cech jak wielodziedziczenie, to ,,przesiadka'' na .NET nie jest możliwa bez ogromnego nakładu pracy na ręczne poprawianie kodu! Z drugiej strony, jeśli rozpoczynamy nowy projekt w C++ z wielodziedziczeniem i chcielibyśmy czerpać korzyści z zalet .NET, należałoby symulować wielodziedziczenie rozmaitymi nieobiektowymi sztuczkami (jedna z nich przytoczona przez EM), mimo że w C++ (bez .NET) nie było to konieczne.
- ... Javie.6
- [EM] Dobra uwaga, erasure model nie jest zbyt dobry przy wielojęzykowości.
W drugiej strony, na podstawie moich wcześniejszych prac z językiem Haskell
przy FFI, mogę zgeneralizować listę obiektów przez zdefiniowanie List
a = ObjectList.
[OPD] Dobrze wiedzieć, że możemy tego tak używać.
- ... typowanie.7
- [EM] Jak powiedziałem, chcemy unowocześniać system wykonawczy, by lepiej
wspierał niestandardowe języki. Zauważ, że CLR już ma parę cech ułatwiających
pracę z takimi językami, np. boxing i unboxing [obj6] (co jest tańsze niż
stosowanie zaślepek do klas) oraz wywołania ogonowe (tail calls). Dodanie wywołań
ogonowych jest odpowiedzią na potrzeby użytowników języków deklaratywnych.
[OPD] Sądzę, że jeśli dodamy lekkie obiekty do Javy (Lightweight Objects, [obj7]), dostaniemy w ten sposób tańszy boxing, jeśli ludzie nie będą upierać się przy stosowaniu osłon (wrappers) jako monitorów.
- ... metod.8
- [EM] Wiele spośród języków Projektu 7 to wyższego poziomu języki deklaratywne. Podobnie jak to jest z dziedziczeniem, sprawa bloków w każdym z nich wygląda trochę inaczej. Dla przykładu, funkcje w Haskell'u są zupełne i leniwe (lazy), podczas gdy funkcje w SML surowe i mogą mieć efekty uboczne. Tak jak powiedziałeś, ważne jest by przy tym projekcie (.NET) nie skupiać się na cechach jednego języka, ale równo traktować wszystkie. Nie jest to trywialne, jednakże wsparcie dla wszystkich wspomnianych języków jest tym, nad czym pracujemy.
- ... założeń.9
- [EM] Większość istniejącego sprzętu zostało zoptymalizowane dla języków
podobnych do C. Jednak, jak sam dalej twierdzisz, nie oznacza to koniecznie,
że nie nadają się dla innych języków. Jeśli zbuduję specyficzny odśmiecacz i
kompilator JIT dla moich języków, może się to okazać w teorii bardziej efektywne.
Jednak pisanie odśmiecaczy, JIT, itd. jest trudną i ciężką pracą, na którą twórcy
wielu akademickich języków nie mogą sobie pozwolić. Ostatecznie, odśmiecacz
ogólnego zastosowania dla wielu języków okazuje się lepszy (wielu z moich znajomych
zaprzestało pracy nad własnymi odśmiecaczami). Mogę stwierdzić, że jakość odśmiecacza
z CLR jest trudna do pobicia.
[OPD] Zgadzam się, że istnienie CLR pomoże ludziom o ograniczonych zasobach. Mam tylko nadzieję, że ta wygoda nie odbierze im wolności i prawa do bycia innowacyjnymi.
- ... językach.10
- [EM] Jak już wspominałem, widać, że nigdy nie uwzględniasz semantycznej
różnicy między językiem a odpowiadającą mu maszyną wykonawczą. Używaniu wspólnej
maszyny wykonawczej jest przyjemną rzeczą, gdyż staje się ZNACZNIE prostsza
konwersja typów przy wymianie danych między językami. Nie musisz synchronizować
dwóch odśmiecaczy (lub z poziomu języka z odśmiecaczem współpracować z językiem
C, który nie ma odśmiecacza), nie musisz zabiegać o zgodne działanie obu systemów
wielowątkowości, itd. Odpada wiele dodatkowych spraw. Zauważ, że wiele pojęć
semantycznych ma sens tylko w obrębie twojego języka, nawet jeśli poruszasz
się w świecie wielojęzykowym. Tak naprawdę to nie oczekuję, że inne języki mogłyby
np. ,,zrozumieć'' w pełni leniwe funkcje. Jednak, ponieważ język współdzieli
środowisko wykonawcze, o wiele łatwiej wbudować twoje własne cechy o szczególnej
semantyce, tak by inne języki mogły z nich korzystać. Na przykład, w naszym
ostatnim artykule o Moridian'ie, w piśmie Dr Dobb's pokazaliśmy jak wbudować
leniwe listy z użyciem iteratorów w C#.
[OPD] W istocie, obraz jaki przedstawiłeś jest to bliski moim wyobrażeniom o tym, w jaki sposób obecnie ludzie używają CLR.
- ...]),11
- [EM] Pytanie, czym jest pojęcie kontynuacji wewnątrz CLR jest ciekawe ze względów badawczych. Na przykład, jak mają się kontynuacje względem modelu bezpieczeństwa. Czy mogę wziąć kontynuację z jednego kontekstu o wykonać ją w innym. Poza tym, różne języki różnie definiują pojęcie kontynuacji (one-shot continuations, shift-reset based continuations,
- ... możliwości.12
- [EM] To jest konwencja CLS, a nie restrykcja CLR.
- ... programu.13
- [JS] W szczególności jest to wygodna cecha np. w języku Python, gdzie możemy dynamicznie dodawać atrybuty klasy.
- ... Javascript14
- [EM] Kompilator Jscript.Net został napisany całkowicie w zarządzanym kodzie (managed code), generuje pełny IL (Intermediary Language) i posiada sensowną szybkość.
- ...Konstrukcje.15
- [EM] Podczas gdy korzystasz z .NET, nadal możesz używać swoich natywnych
bibliotek. Dla przykładu, Moridian Prelude jest cały napisany w Moridian, jednak
teraz z poziomu innych języków też można korzystać z bibliotek Moridian'a. Tak,
jak pokazaliśmy to w artykule w Dr. Dobb's, mogę zdefiniować nieskończoną listę
liczb pierwszych stosując leniwą ewaluację i potem iterować po tej liście z
poziomu języka C#. Program klienta w C# nawet nie dba o to w jakim języku
został zaimplementowany iterator. Z drugiej strony, żaden język nie istnieje
w próżni. Powodem, dla którego języki takie jak Perl, Python, Tcl są bardzo
popularne jest łatwość dostępu z ich poziomu do wielorakich zewnętrznych bibliotek
(takich jak Tk). Posiadanie konstrukcji przeznaczonych do wielokrotnego użytku
jest świetną rzeczą, ponieważ oznacza to, że mogę skorzystać z już raz wykonanej
pracy innych osób i nie muszę rozwiązywać podobnych problemów jeszcze raz, od
podstaw. Idea wielokrotnego wykorzystywania komponentów programowych jest podstawą
wielkiego rynku. Mało kto zamierza teraz od podstaw budować sieć, system szyfrowania,
poczty, GUI. Zamiast tego, języki oferują możliwość przyłączenia istniejących
bibliotek i używania ich w sposób być może bardziej przyjazny niż było to początkowo.
W ten sposób masz do wyboru różne konstrukcje. Możesz użyć swoich już zbudowanych,
możesz napisać coś nowego, korzystając z już istniejących, możesz też zastosować
te udostępnione przez platformę. Myślę, że to jest DOBRA rzecz.
[OPD] Wierzę w to, że w gruncie rzeczy się zgadzamy co do jednego faktu: scenariusze takie jak sprawa wczesnej wersji Eiffel# uczyniły obraz całego .NET bardzo złym. (Niestety, obraz pana Meyer'a także ucierpiał z powodu tej publikacji.) Nie możemy się jednak oszukiwać - trzeba przyznać, że wizja możliwości oferowania tylko jednej (przyjaznej dla CLS) wersji narzędzi/komponentów jest bardzo kusząca dla dostawców. To dostawcy takich produktów wg mnie mogą czerpać z .NET największe korzyści (zauważmy to się już stało na rynku sprzętu; jakie jest wsparcie producentów dla poszczególnych systemów operacyjnych, każdy widzi. -przyp.go języka. Rozumując tak, jeśli byłbym np. programistą C++, ignorowałbym możliwość współpracy mojego produktu z wszelkimi bibliotekami nie używającymi typów zgodnych z STL. Najważniejsze byłoby uproszczenie mojej pracy, a wygoda użytkowników byłaby na dalszym miejscu.
2.3 Garść dowodów
Strona ta zawiera doskonałe omówienie języka Eiffel#, nowego dialektu Eiffel'a wykonanego, aby pokonać ograniczenia CLR (pojawiające się z powodu zbyt ścisłego związku CLR z rodziną języków C/C++/C#):
http://msdn.microsoft.com/library/en-us/dndotnet/html/pdc_eiffel.asp.
Jednym z głównych i zarazem nowych aspektów jakie się tu pojawiają jest brak wsparcia .NET dla tzw. Design-by-Contract [obj14], podstawowej zalety Eiffel'a. Jest pewne, że wsparcie dla Design-by-Contract nigdy się nie pojawi w .NET, gdyż DbC wymaga dyscypliny od programistów (co łatwo wyjaśnia (nie)popularność Eiffel'a), tymczasem Microsoft tworzy SDK ,,dla mas''. Z tego też powodu Eiffel# potrzebował ,,nadbudówek'' dla bibliotek .NET aby móc oferować DbC.
Innym przykładem języka zmodyfikowanego dla .NET jest Smallscript, wywodzący się ze Smalltalka. Zajrzyjmy na grupę dyskusyjną news://news.smallscript.org/. aby poznać parę interesujących wątków jak np. ,,Zabawa z refleksją / Co powinno być w głównym wydaniu?'', gdzie David Simmons, główny naukowiec zajmujący się Smallscriptem, przedstawia ograniczenia wprowadzane przez architekturę .NET:
> O ile rozumiem, z powodu ograniczeń maszyny wirtualnej w .NET,
> zostały zredukowane dynamiczne cechy języka Smallscript na tej
> platformie. Tymczasem wszystkie dynamiczne cechy są dostępne na
> platformie AOS.
Zasadniczo prawda. Spróbuję to wyjaśnić.
Maszyna wirtualna .NET została zaprojektowana dla kompilowanych języków ze statycznym typowaniem. Grupa SmallScript LLC rozwinęła technologię efektywnego wykonywania programów w językach z dynamiczym typowaniem i wiązaniem [łącznie z językami skryptowymi]. ,,Interaktywne'' aspekty języków takich jak Smalltalk są dla aktualnej architektury .NET zbyt trudnym problemem, by dało się udostępnić w nim klasyczne IDE Smalltalka.
(...)
> Czy możemy oczekiwać, że na platformie .NET będą możliwe takie
> rzeczy jak dodawanie klas podczas debugowania?
Nie. Decyzje projektowe i ograniczenia co do czasu trwania projektu .NET powodują, że z obecnej wersji usunięto jakiekolwiek mechanizmy wygodnego debugowania w stylu ,,popraw i kontynuuj wykonanie''.
Takie rzeczy mogą być zrobione, jednak obecnie jest to zbyt skomplikowane. Można założyć, że nie będzie to zrobione przez nas w bieżącej wersji .NET dopóki nie będzie ważnego komercyjnego uzasadnienia, by to zrobić.
Ponieważ konstrukcja .NET źle tu pasowała, ludzie z projektu .NET po prostu zmodyfikowali wirtualną maszynę zoptymalizowaną dla Smalltalka (AOS), tak by współpracowała z maszyną wirtualną CLR i jej binarnymi standardami. Ten sam kod może zostać skompilowany zarówno przez AOS jak i przez CLR .NET-u, lecz .NET nie obsługuje pewnej liczby cech języka. Smallscript czasami trudno utożsamiać ze Smalltalkiem (jest mocno rozszerzonym jego dialektem). Co najmniej w przypadku aktualnych beta-demonstracji, nie ma sposobu na używanie biblioteki klas .NET'u (wywoływane są niskopoziomowe funkcje API Win32: powrót do obsługi komunikatów okien).
Okazuje się ironicznym fakt, że wirtualna maszyna AOS posiada nieporównywalnie lepszą obsługę wielojęzykowości w porównaniu do CLR. Grupa Smallscript LLC planuje oferować przy pomocy AOS obsługę wielu innych języków, takich jak Python i Scheme.
2.4 Trochę kontrprzykładów
Zwolennicy Javy podkreślają fakt, że JVM nie zmusza użytkowników do stosowania języka Java.16 Wirtualna maszyna Javy obsługuje wiele języków (http://cs.tu-berlin.de/~tolk/vmlanguages.html) nawet bez stosowania rozszerzeń potrzebnych do uczynienia jej tak neutralną jak CLR. Krytycy argumentują, że tylko kilka z nich stało się hitami (wyjątkami są JESS, AspectJ, Jython i GJ, które zyskały istotną popularność). Jednak efektywność działania czy nawet integracja w dziedzinie wielojęzykowości (wiele języków na liście posiada spójną integrację z Javą) nie jest powodem, dla którego ludzie wciąż używają Javy.
Ostatnio pakiet Halcyon's Instant Net17 (http://www.halcyonsoft.com/) dodał trochę pikanterii do dyskusji: produkt ten tłumaczy programy wykonywalne .NET-u (w kodzie MSIL) na kod bajtowy Javy! Przetłumaczony kod wymaga do działania JRE (Java Runtime Environment) oraz 3-megabajtowe archiwum .jar, w którym jest implementacja API .NET zbudowana nad środowiskiem Javy. Produkt rozwinął się tak bardzo, że obecnie obsługuje też ASP.NET, WinForms oraz VisualStudio.NET, więc możesz pisać swoje klasy, strony i usługi sieciowe w językach C#, VB.NET lub Eiffel# (lub nawet J#) i umieszczać wedle uznania na serwerach J2EE. Wciąż jest za wcześnie aby rozważać to, czy Halcyon ma wystarczające zasoby, które pozwolą mu nadążać za ewolucją .NET-u oraz czy nowe warstwy środowiska nie zaczną w zbyt dużym stopniu ograniczać efektywności działania.
Obawiam się także, czy Instant Net będzie obsługiwał niezarządzany kod (unmanaged code), jedyny rodzaj kodu w .NET możliwy do łatwej konwersji do kodu bajtowego Javy. Nawet jeśli tak, to istnienie tego produktu (którego wersja beta z powodzeniem uruchamia wiele programów .NET-u) przekonałoby tylko rzesze do wyższości CLR nad JVM w dziedzinie językowej neutralności. Nie wspominając, że dość mały program jakim jest Instant Net (mimo że jeszcze nie kompletny), czyni z .NET prosty klon środowiska Javy, przez to że tak łatwo udaje się go emulować z poziomu Javy.
Przypisy
- ... Java.16
- [EM] W tym miejscu trochę uwag.
(a) Wszystkie omawiane tu języki mogły by być lepsze jako zaimplementowane w CLR ponieważ jest to bogatsze środowisko (por. mój dokument na http://www.research.microsoft.com/~emeijer). Używając J# prawdopodobnie możesz zmodyfikować większość z nich tak by działały w CLR bez większych problemów. [OPD] Jeśli założymy, że JVM nawet nie stara się być pomocne dla innych języków, zaskakującym okazuje się to, że niektóre z języków przeniesionych na JVM działa bardzo dobrze.
(b) Jak powiedziałeś, wiele z tych języków odniesie wzrost wydajności dzięki dodatkowym zaletom spowodowanym kompilowaniu ich w JVM; istotna jest tu chęć stosowania dużej ilości bibliotek.
(c) Na poniższej liście nie ma zbyt dużej liczby języków o komercyjnej jakości. [OPD] Tak, lecz wiele z tych języków realizuje ,,niestandardowe paradygmaty'', więc zapewne nie działałyby dużo lepiej w CLR.
- ... Net17
- [EM] Chciałbym, abyś rozważył wobec produktu Halcyon'a te same zarzuty,
jakie wysunąłeś wobec CLR. CLR posiada wiele cech, które ekstremalnie trudno
byłoby efektywnie przenieść do JVM. Kolejny już raz odsyłam do mojego dokumentu,
gdzie podaję odpowiednie przykłady. Wśród nich są takie rzeczy, jak arytmetyka
z wykrywaniem przepełnienia (JVM tego nie ma), filtry dla wyjątków, delegacje,
zdolność do pobrania adresu argumentów i zmiennych lokalnych, wskaźniki na funkcje,
typy jako wartości, boxing i unboxing, przezroczyste proxy, app domains, wersje
z użyciem silnych nazw. Sądzę, że aktualnie wywołanie niezarządzanego kodu jest
czymś relatywnie prostym do emulacji w JVM używając Java Native Interface. Ostatecznie
wierzę w to, że możliwym jest przetłumaczenia podzbioru języka pośredniego na
kod JVM lecz zostaje wiele pomysłów, które trudno efektywnie przetłumaczyć.
[OPD] Wiele osób zajmuje się modyfikowaniem obsługi arytmetyki w JVM (JavaGrande, JSR83, JSR84,..); może ktoś z nich także doda obsługę przepełnienia arytmetycznego - jest to ważne zagadnienie, niezależnie od sprawy obsługiwania innych języków.
2.5 Wnioski (z oryginalnego artykułu)
Common Language Runtime (wspólne środowisko wykonawcze) jest sprzedawane jako technologia przynosząca wolność, w przekonaniu, że wraz z jej nastaniem wybór języków programowania stał się sprawą drugorzędną. CLR oferuje dla wszystkich języków neutralny system typowania, znakomicie działający kompilator, system wykonawczy i zbiór konstrukcji dla zastosowań biznesowych. VisualStudio.NET dopełnia to, dostarczając pierwszorzędnego zintegrowanego środowiska programistycznego (IDE), zdolnego do rozszerzenie o obsługę dowolnego języka. Wydaje się, że nie ma żadnych ograniczeń w stosowaniu coraz to nowych języków.
Rzeczywistość wygląda dużo bardziej ponuro. CLR nie jest prawdziwie językowo-neutralny, jako że niemal ostentacyjnie faworyzuje języki wyglądające dokładnie jak C#. Te spośród języków, które nie mieszczą się we wspomnianej grupie, są konsekwentnie okrajane i pozbawiane wielu swoich cennych właściwości. Powstają w ten sposób dialekty oryginalnych języków, z których każdy wygląda jak ,,C# ze zmienioną składnią''. Dla przykładu spójrzmy na Eifell# (czy nawet własne języki Microsoft'u - VB.NET i J#). Wybór programistów ogranicza się w ten sposób jedynie do powierzchownych cech, takich jak decyzja, czy rozdzielamy bloki kodu nawiasami ,,{}'', czy może słowami BEGIN i END. Warto także wspomnieć, że CLS/CTS (ogólna specyfikacja języka i typowania) nie pozwala na używanie pełnego zbioru cech maszyny wykonawczej CLR. Na przykład, liczby całkowite bez znaku (unsigned int) są dozwolone przez CLR lecz nie zdefiniowane jako językowo-neutralne tylko dlatego, że wiele języków podobnie jak Java i Visual Basic uwzględnia dualność liczb ze znakiem i bez znaku (nie ma dla tego dobrego rozwiązania).18
Opisany scenariusz powoduje ograniczenie innowacyjności na polu języków programowania, ponieważ cała nowa generacja programistów byłaby szkolona do programowania w językach zgodnych z CLR. To przekazałoby im błędne przekonanie, że wszystkie języki są indentyczne.19 Wszyscy ci ludzie będą znać tylko jeden model dziedziczenia, typowania, budowania struktur, itd. Będą mieli o wiele mniejsze możliwości do eksperymentowania i szukania rzeczywistych alternatyw. Języki nie wystarczająco ściśle zintegrowane z CLR (oraz VisualStudio.NET) staną się trudną barierą w porównaniu z językami prostymi, posiadającymi narzędzia typu RAD. Życie stanie się trudne także dla bibliotek nie spełniających wymagań opisanych w dokumentach .NET-u, włącznie z bibliotekami zaprojektowanymi specjalnie z myślą o przynośności (przenośny kod przecież nie powinien zależeć by od specyfiki .NET).
Bardzo ważną częścią całego problemu jest zamknięta natura CLR.20 Microsoft głośno reklamuje fakt, że C#/CLS ma stać się standardem ustanowionym przez ECMA. Faktem jest też jednak, że to Microsoft (a nie organizacje standaryzacyjne) zachowuje pełną kontrolę nad wszystkimi konstrukcjami i implementacjami. Mniej liczący się producenci oprogramowania nie będą mieli możliwości dostosowywać budowy CLR tak, by lepiej obsługiwał cokolwiek, co nie jest związane z C# czy strategią firmy Microsoft. Producentom (oprogramowania, języków) nie uda się dokonać niezbędnych dla nich poprawek. W rzeczywistości wszyscy programiści .NET będą używać C#, VB.NET lub C++ (o aspektach niewspieranych języków mógłby być napisany cały nowy artykuł). Wciąż te same pięćdziesiąt osób będzie ,,hackować'' na boku w językach takich jak SML czy Dylan, a producenci zaczną czerpać (krótkoterminowe) korzyści z tego, że nie muszą się martwić kompilatorami, back-endami, runtime'ami, IDE czy bibliotekami.
Bawiąc się .NET SDK widać, że obsługa wielojęzykowości wygląda z początku imponująco. Jednak iluzja trwa do momentu, kiedy uświadomimy sobie, że wszystkie języki, które tam udostępniono są w gruncie rzeczy identyczne. Microsoft zdołał wynaleźć koncepcję ,,języka ze skórkami'' (skinnable language): zmieniając najbardziej powierzchowne cechy języka twierdzi, że w wyniku powstał nowy język. W rzeczywistości jest tylko Jeden Prawdziwy Język, którym jest C# oraz ,,skórki'' oferowane przez Microsoft i innych. Tak jak to stało się z graficznymi interfejsami użytkownika, nowe ,,skórki'' zmieniają wygląd środowiska i dodają parę cech, lecz nie są w stanie konkurować z w pełni nowym zestawem narzędzi.
Obecnie znamy wiele ,,językowo neutralnych maszyn wykonawczych'' (CLR), choćby takie jak Pentium, SPARC i inne. Konstrukcja najpopularniejszych procesorów jest dokładnie dopasowana do często bardzo różnych języków programowania. Jest tak dzięki temu, że procesory wykonują tylko najbardziej elementarne (niskopoziomowe) operacje nie wpływające na możliwość użycia ich w konkretnych typach języków.
Nie ma zbyt wielu różnych sposobów wykonywania instrukcji warunkowej. Zdecydowanie więcej jest różnych sposobów obsługi metod, funkcji czy jakichkolwiek konstrukcji znajdujących się w językach wysokiego poziomu. W konsekwencji, każdy język potrzebuje innego kompilatora i systemu wykonawczego aby dało się zaimplementować jego cechy oraz innych bibliotek, aby realizować proponowaną przez niego wizję rozwoju oprogramowania .
Microsoft sprzedaje pomysł, który nie może być dobrze zaimplementowany. Co więcej, ta niedoskonała implementacja łatwo może przynieść więcej szkody niż pożytku. Jak zatem do tego pomysłu porównać technologię Java? JVM jest mniej językowo neutralna, lecz Sun przynajmniej nie twierdzi, że jest ona językowo neutralna. Nawet jeśli poziom językowej neutralności technologii CLR stanie się godny uwagi, podobny poziom będzie mógł być wprowadzony do JVM bez większych zabiegów.21 Zaskakująco dużo problemów może być rozwiązanych przez odpowiednią specyfikację (np. jak konstruować typy wyliczeniowe? można użyć klas, lecz możliwe są też inne rozwiązania). Inną przykładową rzeczą są typy całkowite bez znaku, które wymagają zmian w specyfikacjach maszyny wirtualnej i kodu bajtowego. Niezarządzany kod jest jednie radykalnym odejściem od aktualnej JVM. Istnieje już specyfikacja dla językowo-neutralnego debugowania (http://www.jcp.org/jsr/detail/45.jsp). Jeśli .NET stanie się w przyszłości istotnie konkurencyjną technologią, dodanie wielu innych rozszerzeń do systemu wykonawczego Javy i jej specyfikacji nie będzie wymagało zbyt dużo pracy.
Zwolennicy niepospolitych języków, którzy kupili niebiańską wizję Microsoftu, nauczą się paru rzeczy w dość nieprzyjemy sposób. Większość alternatywnych języków, które odniosły sukces to języki skryptowe, na obu platformach. Widzi się wiele ludzi używających narzędzi takich jak Jython, Jruby pod kontrolą JVM lub Smallscript pod kontrolą .NET. Jest tak, ponieważ zarówno Java jak i C# niezbyt nadają się do zadań ,,skryptowych'', jak np. pisanie dynamicznych stron. Niszowy ,,rynek'' jak zawsze okazuje się łaskawy dla bardzo specjalizowanych języków (JESS, narzędzie dla systemu eksperckiego działające z JVM jest dobrym przykładem). Kiedy jednak przychodzi dla takich narzędzi czas konkurowania w obszarach języków ogólnego zastosowania, nietypowe języki mają mizerne szanse na zdobycie popularności.
Powyższe rozważania nie upoważniają do stwierdzenia, że .NET nie jest dobrą platformą, CLR nie jest dobrą architekturą czy C# dobrym językiem. Osobiste preferencje użytkowników i próba czasu zadecydują o jakości i przydatności platformy .NET. Na razie można ją postrzegać jako wywierający duże wrażenie kawał inżynierskiej roboty oraz, co najważniejsze, długo oczekiwane zastąpienie aktualnej platformy firmy Microsoft. Na nieszczęście proces ten wciąż jest realizowany w typowy dla Microsoft'u (i marketingu) sposób: zamknięta technologia ukazywana jest jako otwarta oraz mocno ograniczony w funkcjonalności system prezentuje się jako językowo neutralny.
Przypisy
- ... rozwiązania).18
- [EM] To nie jest złe. CLS określa reguły ułatwiające komunikację między
różnymi językami; w pewnym sensie celem CLS jest zapewnienie, że nie każdy język
spełnia pełną CTS. Nie oznacza to jednak, że wewnątrz programu w swoim języku
nie możesz używać cech niedozwolonych przez CLS.
[JS] Można stąd wywnioskować, że ten kto kontroluje standard CLS ma też kontrolę nad tym, jakie języki ,,dopuszcza do spółki'' w .NET lub przynajmniej w jakim stopniu mogą one z niego korzystać. Nie jest to raczej bezpieczna dla wszystkich reguła. Proste dodanie do specyfikacji CLS/CTS paru nieogólnych warunków spowodowało, że używanie w .NET wielu ,,niestandardowych'' języków ma o wiele mniej sensu w porównaniu do używania C#. Oczywiście to, że specyfikacja CLI została zarejestrowana w ECMA (organizacja ustanawiająca standandardy teleinformatyczne) pozwala z większym optymizmem patrzeć w przyszłość. Należy jednak pamiętać, że ten kto posiada dominującą implementację danego standardu, de facto kontroluje ten standard.
- ... indentyczne.19
- [EM] Ja mam nadzieję, że nowa generacja programistów będzie rosła z myślą,
że istnieje wiele języków programowania i każdy ma swoje silne punkty.
[OPD] Obyś miał racje.
- ... CLR.20
- [EM] Kontynuujemy współpracę z producentami języków, środowiskiem akademickim
i innymi w celu unowocześnienia platformy. W marcu odbędzie się kurs organizowany
przez MSR Cambridge, gdzie naukowcy będą mogli poznać możliwości korzystania
z implementacji CLR na licencji Shared Source:
http://research.microsoft.com/programs/europe/events/dotnetcc/
- ... zabiegów.21
- [EM] Dlaczego nie miałoby być tak też w przypadku CLR?
[OPD] Widać, że w CLR zaimplementowano już większość konstrukcji międzyjęzykowych, które mają praktyczny sens. Jestem przekonany, że podobna zmiana JVM w kierunku wielo-językowości byłaby relatywnie prosta (oczywiście fakt, że wy już zrobiliście taki krok upraszcza sprawę). Z drugiej strony wiele spraw, o których tu dyskutujemy jest wystarczająco trudnych, jako że: (a) nie mam pomysłu jak powinny być rozwiązane, (b) Microsoft nie dba o zajmowanie się nimi, przynajmniej w bieżącej wersji .NET. Oczywiście, jesteście na dużo lepszej pozycji, ułatwiającej wprowadzanie rozszerzeń, więc rozumiem Twój optymizm.
3 Załączniki: Dyskusje, poprawki, objaśnienia
Oryginalny artykuł wywołał dużo interesujących komentarzy i dyskusji pochodzących od czytelników JavaLobby, Slashdot'u oraz reakcję ekspertów związanych z technologią .NET. Ujawniło się w ten sposób trochę pytań oraz faktów mogących wpłynąć na opinie czytelnika. W każdym razie akceptuję inne punkty widzenia nawet, jeśli nie zmienia to mojego stanowiska.
3.1 Bramki logiczne nie są takie złe!
Weźmy pod uwagę najbardziej zaskakującą sprawę - niektórzy dyskutanci krytykowali nawet moje (jak myślę typowe) przekonanie, że procesory ogólnego zastosowania są obecnie jedynymi językowo-neutralnymi systemami wykonawczymi (nie ograniczonymi żadnym istotnym związkiem z konkretnym językiem programowania). Ponieważ dzisiejsze procesory wciąż są budowane w oparciu o model obliczeń Von Neumanna, rzecz jasna faworyzują przez to języki imperatywne, czyniąc trudniejszym ,,życie'' języków funkcyjnych. Cóż, nie jestem przeciwny powstawaniu sprzętu, który posiadałby bezpośrednie wsparcie dla języków funkcyjnych. Oczywiście wiem o maszynach LISPowych, Xerox Dorado i innych próbach oferowania sprzętowego wsparcia dla języków innych niż imperatywne. Jednak te próby się nie powiodły.
Procesory dedykowane dla konkretnych języków wysokiego poziomu oferują głównie niskopoziomową obsługę najważniejszych czynności z punktu widzenia systemu wykonawczego, takich jak wsparcie dla odśmiecacza czy bezpośrednia interpretacja instrukcji maszyny wirtualnej. W związku z tym nie jest wtedy już potrzebny złożony interpreter czy kompilator. Technologia taka jednak aż do lat 80. nie istniała, co więcej była zbyt kosztowna dla docelowego sprzętu (np. dla urządzeń J2ME). Dobre kompilatory (jeśli są dostępne) zawsze przewyższają specjalizowane procesory w oferowanym wsparciu dla wysokopoziomowych instrukcji maszyn wirtualnych. Tutaj można przypomnieć konkurencję technologii CISC kontra RISC. Ta druga, mniej specjalizowana wygrała.22 Inny przykład: Sun zrezygnował z picoJava na rzecz projektu MAJC.
Dedykowany sprzęt nie jest w stanie rozwiązać fundamentalnych problemów, jak np. zapobieganie niepoprawnym przypisaniom. Dobre kompilatory potrafią wziąć kod w języku funkcyjnym i wykonać translację na jego odpowiednik imperatywny, gdzie np. tworzenie nowych obiektów dla każdego kroku obliczeń może być zastąpione wielokrotnym użyciem tych samych obszarów pamięci. Model Von Neumanna jest związany raczej z prawami fizyki niż popularnością języków imperatywnych. Ważną rzeczą, której się nauczyłem podczas mojej edukacji informatycznej jest to, że obszar informatyki nie pokrywa się bezpośrednio z obszarem matematyki... Miło by było, gdyby procesory mogły bezpośrednio wykonywać rachunek lambda, lecz jak na razie nie zanosi się na to.
Przypisy
- ... wygrała.22
- [JS] RISC na pewno jest zwyciężcą technologicznym lecz niestety nie rynkowym...
3.2 Uogólnienia, C++ i Eiffel raz jeszcze
Otrzymałem informacje na temat ogólnej specyfikacji C# (Generic C#, GC#): http://research.microsoft.com/projects/clrgen/.
Autor bardzo dobrze przedstawił zagadnienie. Jest przekonany, że GC# zyska kilka przewag nad Generic Java, GJ (obie specyfikacje nie są jeszcze ukończone). Zgadzam się w kilku sprawach, szczególnie co do obsługi dla typów prymitywnych. Obaj jesteśmy zaskoczeni, że w GJ wciąż tego nie wprowadzono. (Język proponowany do zaadoptowania w JDK1.5 jest pozytywnym rozszerzeniem, jednak wydaje się zaledwie nieśmiałym pierwszym krokiem po którym wielu programistów z niepokojem będzie czekać na następną wersję). Z drugiej strony, erasure model w GJ może być najlepszym rozwiązaniem problemów ze zgodnością.23 Prototypowe wydanie .NET-u zawiera pełną bibliotekę kolekcji, więc znajdujemy to ten sam problem jaki jest z Javą. Dotyczy on sytuacji, kiedy różne programy mają mieć dostęp do tego samego typu Lista (pomijamy względy implementacji) wyrażonego zarówno w postaci beztypowej kolekcji (typu podstawowego Object), jak też wyrażonego w postaci Lista<T> (mimo wszystko, beztypowa postać kodu bajtowego powinna wyeliminować trochę problemów). I tutaj dygresja! Wracając do .NET-u, widoczne zaawansowanie stanu w jakim się znajduje omawiana specyfikacja GC#, potwierdza moje stwierdzenie, że Microsoft będzie miał ścisłą kontrolę nad specyfikacjami nawet po tym, gdy staną się ,,otwartymi standardami''.
Przyznałem wcześniej, że stosowanie C++ (i innych języków) jest dobrym rozwiązaniem jedynie wtedy, gdy można efektywnie wykorzystywać właściwe im cechy. Niektórzy zwolennicy ,,okrojonego C++'' nie zgodziło się z tym stwierdzeniem. Szanuję ich preferencje, jednak odpowiedziałem im następującym komentarzem (z JavaLobby):
,,Problemem języka C++ jest to, że w momencie gdy ktoś zdecyduje <<stosujmy tylko najnowsze i najciekawsze cechy>>, spowoduje to problemy (uwzględniając niektóre scenariusze i punkt widzenia niektórych osób), m.in. z działaniem języka w platformie .NET. Z drugiej strony, będąc rozsądnym programistą takie zabiegi są zazwyczaj jedynym sposobem uczynienia z C++ wystarczająco bezpiecznego języka. Spróbujmy zacząć dyskusję o zarządzaniu pamięcią w C++ (np. sprawy jej zwalniania), błędach związanych z wskaźnikami i optymalizacjami kompilatora (powodowanymi też starym przyzwyczajeniem np. do wykonywania niejednoznacznego rzutowania), odśmiecaczach dla C++ itd. Natychmiast otrzymamy tysiące odpowiedzi zwolenników C++ przekonujących o tym, że żaden ze wspomnianych problemów nie zaistnieje, jeśli będziemy radykalnie stosować tylko nowy paradygmat C++ (pochodzący z następnych po ANSI i STL standardów). Oznacza to stosowanie szablonów, STL, ,,sprytnych wskaźników'', nowych sposobów rzutowania oraz wszystkich pozostałych nowych, przyjemnych rzeczy; unikanie natomiast jak ognia stosowania wszelkich cech starych i destrukcyjnych. W ostateczności, aby sprawić, by C++ był językiem przyzwoitym - w opinii użytkowników C++ - należy stosować najnowsze rozwiązania, bez względu na koszt ich poznania czy przenośności.'' [Łącznie z kosztem zgodności międzyjęzykowej i kosztem znalezienia kompilatora wspierającego nowy standard w pełni i ściśle przy jednoczesnym nieobsługiwaniu ,,starych, złych konstrukcji''].
Teraz parę słów o Eiffel'u. Zostałem powiadomiony przez ISE (Interactive Software Engineering, twórców języka Eiffel, przyp. [JS]), że Eiffel#, pierwszy (zmodyfikowany) dialekt języka Eiffel dla .NET, nie jest już rozwijany czy oferowany. Zamiast tego ISE oferuje standardowy Eiffel działający w .NET. Oznacza to, że muszę ograniczyć mój wielki krytycyzm dla Eiffel#, ponieważ Eiffel# uważany jest teraz tylko jako prototyp. Nowy Eiffel dla .NET nie jest zaskoczeniem, gdyż język ten jest bliższy względem C / C++ / C# niż wiele innych języków (ja postrzegałem Eiffel'a, przynajmniej na początku jako ,,dobrze zrobione C++ z dodaną cechą Design-by-Contract''). Po zmianach implementacji nadal jednak problemem jest językowa neutralność; co prawda można używać wielodziedziczenia, lecz to jeszcze nie świadczy o językowej neutralności. Inne zagadnienia nadal są ograniczeniem podczas przekraczania barier językowych (typy parametryzowane, kontrakty). Sytuacja jest podobna jak ta z C / C++ / Managed C++; można robić wszystkie rzeczy dozwolone przez język, dopóki nie potrzebujemy językowej neutralności albo (w gorszym przypadku) dopóki używamy kodu niezarządzanego lub osobnej (oprócz CLR) maszyny wirtualnej. Z mojego punktu widzenia są to brudne sztuczki - to tak, jakby aplikacja w Javie używała natywnych rozszerzeń, aby dało się wykonywać coś, czego w Javie się nie da. Istnieją narzędzia do robienia czegoś takiego, np. hybryda SuperCede Java/C++, rozszerzenia BulletTrain (używane wewnętrznie by zaimplementować VM w samej Javie), badania numeryczne IBM'a oraz jakiekolwiek rozszerzenie korzystające z Java Native Interface (JNI). Nawet przy stosowaniu takich rozszerzeń pamiętajmy, że ,,reszta systemu'' nie jest zaprojektowana z myślą o cechach danego języka. Przez to jesteśmy zobowiązani do ciągłego zapewniania, że używane przez nas ,,natywne'', tradycyjne biblioteki nie będą zastępowane przez zestaw bibliotek wbudowanych w .NET. Stosowanie takich konstrukcji utrudnia komunikację między obiektami. Akceptowane przez nas dotąd narzędzia24 mogą produkować kod od nich zależny (mimo, że są one standardowe z naszego punktu widzenia). Na przykład, językowo neutralne narzędzie do budowy GUI nie powinno generować kodu zależnego od klas biblioteki WinForms (lub konkretnej architektury), ponieważ nie jest to zestaw preferowany przez wszystkie stosowane języki.
Ostatecznie, morał jest taki: używaj dowolnych cech i bibliotek odpowiednich dla Twojego języka oraz definiuj proste interfejsy wymiany informacji ze światem zewnętrznym tak, by stosowanie tej komunikacji nie wymagało zbyt dużo pracy. Takie podejście działa równie dobrze z CLR jaki i z Javą (w połączeniu z JNI, CORBA czy innym interfejsem). Wygląda na to, że implementacja nowego Eiffel'a dla .NET będzie dobrym środowiskiem dla programujących w tym języku, lecz nigdy nie stanie się on prawdziwie językowo neutralny. Nawet sama możliwość uruchamiania programów Eiffel'a na CLR będzie zależeć od szerokiej warstwy rozszerzeń i nowych bibliotek. W ten sposób znacznie oddalono się od oryginalnej wizji platformy, gdzie ,,pluginy dla języków'' miały być dostarczane po prostu jako kompilatory do kodu MSIL plus pewne rozszerzenia VS.NET dotyczące podświetlania składni i debugowania. Co gorsze, obecność warstw zakrywających dodatkowe biblioteki języka spowoduje zwiększone wykorzystanie pamięci i mocy obliczeniowej.
Przypisy
- ... zgodnością.23
- [EM] Jednak cierpi z wielu powodów wcześniej przez Ciebie przytoczonych, np. odpowiadające językowi środowisko wykonawcze nie obsługuje bezpośrednio tej cechy, więc nie możesz jej tego po prostu skompilować.
- ... narzędzia24
- [JS] Pomyślmy choćby o kosztownych, często posiadanych już przez nas narzędziach CASE. Ich uaktualnienie (jeśli będzie dostępne) i wdrożenie wymagałoby dodatkowych inwestycji.
3.3 Dobrze, przyznaję się - ALE...
Aby być uczciwym stwierdzam, że wierzę w możliwość istnienia pewnego rodzaju ,,uniwersalnej maszyny wirtualnej''. Wspomniałem już podejście nisko poziomowe: procesor. Innym rozwiązaniem jest bardzo elastyczna architektura, CLOS (Common Lisp Object System, bardzo popularny wśród badaczy języków, ponieważ umożliwia stosowanie dowolnego modelu obiektowości, systemu typowania i struktury behawioralnej).25 Niuansem jest tutaj (zgadnij co?) - efektywność. Każdy kompilator i system wykonawczy posiada sporą liczbę ,,uzasadnionych uproszczeń'', jak np. rezygnacja z niektórych cech języka czy założenie, że zazwyczaj będzie miało miejsce tylko kilka z wielu możliwych scenariuszy wykonania, przez co warto skupić się tylko na ich maksymalnej efektywności. Nadmiar elastyczności kompilatora często powoduje kiepską wydajność programów.
W przypadku JVM i CLR, samo istnienie typów prymitywnych jest jednym z lepszych przykładów opisujących powyższe problemy. Wpływa ono na potencjalną niemożliwość implementacji maszyny wirtualnej w przybliżeniu tak samo dobrze uruchamiającej programy w wielu rozmaitych językach. (Najgorszy problem z typami prymitywnymi jest, z punktu widzenia językowej neutralności, ich sztywna definicja a nie inne sprawy niepokojące purystów od obiektowości). Mógłbym wyrazić się jaśniej: CLR nie jest doskonałym poziomem neutralności językowej (nawet jeśli lepszym w tej dziedzinie niż JVM); nie mam pojęcia w jaki sposób przyszłe (wstecznie kompatybilne) wersje mogłyby być ulepszone w wielu ważnych zagadnieniach, dopóki nie nastąpi jakiś naprawdę rewolucyjny rozwój.
Polecam lekturę http://research.microsoft.com/~emeijer/Papers/CLR.pdf. Autorzy publikacji stwierdzają ,,Byłoby nadużyciem twierdzić, że CLI w obecnej postaci jest gotową, bezproblemową platformą obsługującą wiele języków. Aktualnie ma ona dobre wsparcie dla imperatywnych (COBOL, C, Pascal, Fortran) i statycznie typowanych języków obiektowych (takich jak C#, Eiffel, Oberon, Component Pascal). Microsoft prowadzi współpracę z twórcami implementacji języków oraz badaczami, w celu polepszenia obsługi języków o niestandardowych paradygmatach''. Powyższe stwierdzenie tym, czego oczekujemy od poważnych materiałów badawczych; widać, że nastrój jest tu podobny jak w moim oryginalnym artykule. Zauważmy jednak, iż języki nie należące do rodziny ,,imperatywnych, statycznie typowanych'' określone zostały jako ,,niestandardowe''.
Firma ActiveState przedstawiła raport z jej doświadczeń w implementowaniu języka Python dla .NET (przy wsparciu Microsoft'u): http://www.activestate.com/Initiatives/NET/Research.html. Co ciekawe, dokument ukazuje kilka międzyjęzykowych problemów, które nie przyszły mi na myśl. Autorzy kończą konkluzją, że projekt powiódł się, gdyż jest możliwość działania programów Python'owych w .NET, mimo że jest to okupione pewną liczbą problemów z komunikacją i wydajnością: ,,Szybkość działania bieżącej implementacji jest tak niska, że nie nadaje się ona do niczego oprócz prezentacji możliwości''. Niektóre problemy związane są z niedojrzałością aktualnej implementacji Python'a dla .NET, lecz wiele innych są ograniczeniami CLR. Python nie jest oczywiście ściśle związany z C#. Może się to zmienić po tym jak firma ActiveState ogłosiła zamiar wprowadzenia zmian do implementacji języka - deklaracje typów mają stać się statyczne. Mam nadzieję, że nie jest to pierwszy krok ku uczynienia Pythona kolejną ,,skórką dla C#''. Krytycy narzekają na złą efektywność wersji beta platformy .NET. Tymczasem można to zestawić z wrażeniem towarzyszącym projektowi Jython. Ta implementacja Pythona dla JVM (wykonana bez wielkiego budżetu i bez pomocy ze strony dostawcy maszyny wirtualnej) osiąga 50% wydajności swojego odpowiednika implementowanego w C (dojrzałego od wielu lat). Oferuje ona integrację nawet z dynamiczną hierarchią klas (two-way subclassing) i znakomicie działa na maszynie wirtualnej, która z założenia oferuje gorszą obsługę wielojęzykowości. W artykule Jona Udell'a otrzymałem (niespodziewanie pozytywny) odzew ze strony autorów języków dla JVM: http://www.byte.com/documents/s=505/BYT20001214S0006/index.htm.
Podsumowując dodam, że w przypadku prawdziwie językowo-neutralnych maszyn wirtualnych nikt nie powinien być zmuszony do poświęcenia długich lat na dodatkowy rozwój i optymalizowanie języka tak, by działał dobrze (w porównaniu do ,,standardowej implementacji''). Nie mogę być zbytnio oczarowany językiem X działającym efektywnie i ,,prawie pezproblemowo'' na CLR, jeśli efekt został okupiony olbrzymią pracą programistyczną i kupą hakerskich sztuczek. Językowo-neutralna maszyna wirtualna powinna umożliwiać efektywną kompilację jakiegokolwiek języka w niewiele gorszym stopniu niż analogiczna kompilacja dla zwykłego procesora.
Otrzymałem dalszy odzew z firmy JNBridge
(http://www.jnbridge.com),
której ideą jest implementacja pomostu miedzy JVM a CLR tak, by kod klas Javy
w dalszym ciągu wykonywał się w JVM, jeśli tak jest wygodniej, przy czym mogłyby
one w sposób przezroczysty komunikować się z klasami .NET'u (poprzez automatycznie
generowanych po obu stronach pośredników). Jest to bardzo interesujące podejście,
pozwalające cały kod Javy uruchamiać nadal w jej natywnym systemie wykonawczym
i w ten sposób zapobiegać jakomkolwiek kłopotom z efektywnością, architekturą
czy kompatybilnością. Całkiem zaskakujące okazało się, że zadanie to nie jest
proste nawet z architekturą Javy. Jest spora lista problemów może nie krytycznych,
ale dotyczących choćby komunikacji między językami (międzyjęzykowe wywoływanie
i tworzenie podtypów):
,,- Interfejsy Javy pozwalają na włączenie stałych (pól static final).
Interfejs .NET nie zezwala na włączanie takich pól.
- Abstrakcyjne klasy Javy implementujące intefejs nie wymagają załączania
definicji każdej metody interfejsu; każda nieabstrakcyjna klasa dziedzicząca
z klasy abstrakcyjnej musi już posiadać definicje wszystkich zadeklarowanych
metod, gdziekolwiek w łańcuchu dziedziczenia. Tymczasem w .NET CLR definicje
klas abstrakcyjnych muszą zawierać implementację (choćby pustą) wszystkich swoich
metod.
- W .NET CLR pole klasy nie może być jednocześnie static i final.''
Była możliwa prosta translacja, jednak nie jest akceptowana przez weryfikator CLR, więc wymyślono obejście problemu. Jednakże: ,,Obejściem problemu była modyfikacja struktury klasy wynikowego pośrednika po stronie .NET, przez co struktura klasy .NET nie była dokładnym odzwierciedleniem oryginalnej struktury klasy Javy. W ten sposób okazało się nieprawdą twierdzenie, że .NET CLR jest językowo neutralny.'' Jako konkluzję podano: ,,Przy ewentualnym tworzeniu kompilatora Javy dla MSIL i .NET w pełni zgodnego ze standardami, napotkałoby istotne trudności.''
Przypisy
- ... behawioralnej).25
- [EM] To brzmi trochę niebezpiecznie!
[OPD] Być może, jednak moje (na tym polu ograniczone) doświadczenie mówi mi, że CLOS jest bardzo dobry do celów badań nad językami programowania. Przez to najczęściej jest używany właśnie do tego - przypomina mi się tutaj dowcip o ML'u, języku, który jest ,,bardzo popularny przy pisaniu kompilatorów ML'a''...
3.4 Wnioski (drugie podejście)
Jeszcze raz wspomnę - nie zamierzałem argumentować, że CLR nie jest rozwojowy (w porównaniu choćby do JVM) na polu wielojęzykowości. Osoby ograniczające się jedynie do niewielkiego zbioru języków (co czyni z nich najbardziej popularne języki) bez wątpienia otrzymają ze strony CLR to czego oczekiwali. Nawet w takim przypadku programiści nadal będą używać tradycyjnych bibliotek i narzędzi związanych z ich językiem a nie bibliotek i narzędzi pochodzących z .NET, o ile starają się stosować język świadomie. Jedynie wtedy, gdy nadal będziemy korzystać ze standardowych bibliotek danego języka (oraz w pewnych przypadkach z dostarczonego z nim IDE), można będzie w pełni wykorzystać nabyte już doświadczenie, przenośność kodu, oryginalne wsparcie oraz zalety płynące z oferowanego przez język paradygmatu programowania. Jeśli preferujesz nowe środowiska programistyczne Microsoft'u, zapewne powinieneś używać języka C# lub może VB.NET. Nie mogę jednak zrozumieć, dlaczego ktokolwiek miałby używać w tym środowisku innego języka. Jeśli programujesz, powiedzmy w C#, środowisko .NET i VisualStudio.NET jest najlepszym wyborem (przynajmniej dla programowania pod Windows), ponieważ w ten sposób bez żadnych kompromisów używasz spójnego zestawu narzędzi.
Nie ma nawet potrzeby, aby zajmować się egzotycznymi językami: spojrzenie na sam VisualBasic.NET wystarcza do rozpoznania przepaści między tym, co .NET obiecuje a tym co w rzeczywistości oferuje. Poprzednie wydanie VB6 nie przedstawia żadnych ważnych problemów, o których tutaj dyskutowano; jest to bardzo prosty język z bezpieczną obsługą pamięci i ,,standardowym'' modelem obiektowości i typowania. Nie ma tu jakichś dziwnych wymagań, są tylko małe idiotyzmy w stylu (nie działających) typów logicznych czy (całkiem zabawna) opcja ustalająca podstawę od której zaczynamy indeksowanie tablic (.NET lubi wyłącznie tablice rozpoczynające się od 0). Nie uwzględniając nawet tego, że Microsoft nie ulepszył nowej wersji VB.NET, podstawowym i krytycznym błędem jest niezachowanie sensownej wstecznej kompatybilności z VB6.
Co zabawne, sprawy zaszły jeszcze dalej: Microsoft zapowiedział wsparcie dla ponad 20 języków, jednak wiele osób ze zdziwieniem odkryło, że są to głównie języki nowe - zarówno języki stworzone specjalnie dla .NET jak też dialekty istniejących języków poddane znacznym modyfikacjom, rozszerzeniom i ograniczeniom. Nigdy nie słyszałem, by ktoś chciał modyfikować język C++ tylko dlatego, że chce kompilować dla maszyn Solaris/SPARC, zmieniać coś w definicji Smalltalk'a, aby móc uruchamiać go na Mac/PPC czy też okroić Eiffel'a, by używać go na Microsoft Windows/x86.
Krytycy zarzucają Microsoftowi chęć zablokowania możliwości swobodnego stosowania języków programowania po tym jak zablokował konkurencyjność wielu platform: w ten sposób programiści mogliby zacząć używać specyficzne dla .NET'u dialekty i biblioteki, czyniąc trudniejszym tworzenie kodu niezależnego od platformy nawet przy pomocy języków, które zawsze to umożliwiały (przynajmniej na poziomie kodu źródłowego). Co gorsze, Microsoft ma wystarczające środki, by wpłynąć na ewolucję wszystkich zagrożonych języków wydając wciąż nowe wersje systemów wykonawczych .NET'u i bibliotek. Jeśli chodzi o respektowanie otwartych standardów, Microsoft jest znany ze swojej ignorancji. Nie trzeba nawet wspominać MSJavy: Kiedyś byłem programistą VC++ i na co dzień spotykałem się z niezgodnością kompilatora ze specyfikacją ANSI; sądząc po najnowszych wrażeniach Ala Stevens'a, Microsoft nadal pomija tak ,,starożytne'' już cechy jak namespace'y. Długie lata może zająć proces powierzania sprawy językowych standardów (w szczególności tych nie kontrolowanych przez Microsoft) niezależnym organizacjom. Sun sprawuje kontrolę nad pojedynczym językiem, który sam stworzył.
Należy przyznać, że w architekturze CLR możliwe i oczekiwane są pewne usprawnienia, nawet jeśli nie wierzysz w możliwość istnienia (lub sens) prawdziwie wielojęzykowego środowiska. Spójna integracja z COM+ jest czymś, co bardzo chciałbym posiadać, korzystając z mechanizmów CORBA (które w językowej neutralności okazują się mieć przewagę względem COM). Kilka cech systemu wykonawczego, takich jak jawna obsługa wywołań ogonowych mogłaby być zaadoptowana w JVM, czyniąc ją lepszym systemem (choćby tylko dla języka Java) bez większych kompromisów. Jedna z największych przewag (i problemów) Microsoft'u to dostarczanie kiepskich rozwiązań przy ciągłym ich przeprojektywowywaniu i reimplementowaniu do czasu, aż będą sensowne. Sun jest o wiele bardziej konserwatywny i ostrożny jeśli chodzi o ewolucję głównych specyfikacji języka Java. Bywa to nawet aż przesadne (formaty plików .class i .jar są moimi prawdziwymi zmorami, chciałbym aby Sun przeprojektował od początku swój format binarny, wersjonowanie i pakowanie). Platforma .NET to nie jedyny produkt reklamowany obietnicami obsługi języków i otwartością. Zmodyfikowanie Javy do postaci klonu .NET'u nie jest właściwym krokiem (równie dobrze można pomyśleć o zaadoptowaniu .NET'u); nawet jeśli technologia Microsoftu stanie się dojrzała i odniesie sukces, nie można godzić się na kompromisy, które z nią się wiążą. .NET skopiował wiele najlepszych idei Javy i jest to dobre dla rozwoju, lecz nie może oznaczać konieczności ślepej wiary w ,,jedyną właściwą'' wizję rozwoju oprogramowania.
4 Wyjaśnienia trudniejszych terminów
Większość terminów dotyczy elementów programowania obiektowego. Wyjaśnienia
umieszczono według kolejności pojawiania się danego terminu w tekście.
Szczególne podziękowanie za cenną pomoc należy się Piotrowi Sawickiemu.
W wielu przypadkach korzystano ze ,,Słownika
terminów z zakresu obiektowości'' autorstwa Kazimierza Subiety, dostępnego
także w Internecie. Wybór terminów jest w pełni subiektywny, więc w razie potrzeby
odsyłam do lektury wspomnianego słownika.
Jarosław Staniek, maj 2002
1. Inner classes - klasy wewnętrzne, zagnieżdżone.
2. Mix-ins (mix-in classes) - klasa mieszana. Klasa abstrakcyjna, która jest używana w celu uzupełnienia interfejsu lub funkcjonalności innych klas. Klasa mieszana wymaga wielodziedziczenia.
3. Programowanie generyczne (generic programming),
typy generyczne, parametryzowane (generic types).
Ogólnie - generyczny: określenie procedury, funkcji, klasy lub techniki
programowania, która może być użyta w szerokim obszarze zastosowań, akceptuje
różne typy argumentów, rodzaj przetwarzanych danych, kontekst aplikacyjny, itd.
Np. generycznych procedur wymaga interfejs umożliwiający przeglądanie dowolnej
relacyjnej bazy danych, generyczną jest funkcja zwracająca rozmiar zbioru elementów
dowolnego typu, generyczny jest analizator leksykalny, który można zastosować
do wielu języków, generyczny jest program, który jest w stanie wykonać dowolne
zapytanie w SQL wczytane z klawiatury, itd.
Programowanie generyczne jest techniką tworzenia programów operujących
nie na konkretnej strukturze danych, ale na szerokiej rodzinie takich struktur
(określonych typami generycznymi, parametryzowanymi). Przykładami programów
generycznych są: program sortujący dowolną listę, program tłumaczący dowolny
program w języku Pascal na program w C, program przeglądania tablic w bazie
danych, w którym nazwa tablicy jest parametrem wczytywanym z klawiatury, itp.
Programowanie generyczne sprzyja ponownemu użyciu (reuse). Wiele języków
programowania (np. Pascal, C#) nie jest przystosowanych do tego typu programowania.
Znanych jest kilka technik programowania generycznego, np.: przejście na niższy
poziom językowy (np. assemblera) poprzez operatory konwersji typu, polimorfizm
powiązany z procedurami wyższego rzędu (procedurami z parametrami będącymi procedurami),
klasy parametryzowane (szablony, templates) oraz wykorzystanie refleksji
(reflection) (np. w dynamicznym SQL).
4. Erasure model - chodzi o uogólnianie typów - sposób uzyskania mechanizmu szablonów w Javie. Po zbadaniu do jakich typów jest wykorzystywany szablon, bez zapamiętywania w kodzie argumentów szablonu, stosuje się najbliższego wspólnego przodka dla danych użyć szablonu. Np. dla klas Zwierze<jamnik> i Zwierze<pudel> może powstać klasa ZwierzePies.
5. Covariance - ,,zawężanie polimorfizmu'', oto przykład w języku Eiffel:
class PARENT
feature foo (arg: A) is ...
end; - PARENT
class CHILD inherit PARENT
redefine foo
feature foo (arg: B) is ...
end; - CHILD
Zgodnie z regułą covariance, klasa B powinna dziedziczyć z A, a nie odwrotnie, gdyż byłoby to wbrew kierunkowi dziedziczenia klas (CHILD inherit PARENT). W przeciwnym przypadku mamy do czynienia z regułą contravariance.
6. Class boxing/unboxing - mechanizm w systemie typowania, np. języka C#, umożliwiający obustronne przechodzenie między typami opartymi o wartości i typami opartymi o referencje do obiektów. Boxing - konwersja z typu opartego na wartości na typ referencji. Unboxing - przeciwna operacja. Oto przykład obu operacji:
int i = 1;
object o = i; // boxing
int j = (int) o; // unboxing
Omawiany mechanizm pozwala traktować w szczególności liczby całkowite jako obiekty lub jako zwykłe (np. 32-bitowe) wartości, zależnie od potrzeby.
7. Lightweight objects - sposób reprezentacji obiektów przez maszynę wykonawczą języka, zmniejszający narzuty pamięciowe bądź obliczeniowe (np. przez współdzielenie danych z innymi obiektami - analogia do wątków jako ,,lekkich procesów''). Najczęściej są obiektami zajmującymi mało pamięci (np. klasy liczb zespolonych, wektorów) i powoływanymi do życia często jako tymczasowe, na krótki czas, więc dąży się do optymalizacji kosztu ich użycia (np. przez stosowanie metod inline).
8. Single-receiver dispatch - dopuszczenie przez język poliformiczności jedynie względem odbiorcy komunikatu (określonym przy wołaniu metody zazwyczaj niejawnym argumentem this lub self), por. multi-methods.
9. First-class methods - metody, na których można dowolnie operować, a nie tylko je wywoływać (np. wskaźniki na funkcje w C); metody będące obiektami.
10. Multi-methods - wielo-metody, metody gdzie wywołanie jest polimorficzne na kilku argumentach (a nie tylko jednym, odpowiadającym odbiorcy).
11. Reflection - refleksja, tworzenie odbić. Technika programowania, w której programista pisze program generujący kod programu (w tym samym lub w innym języku), składając go ze stałych stringowych, danych, wejścia z klawiatury, metadanych zapamiętanych w pewnym repozytorium, itd. Wygenerowany program (zwykle w postaci ciągu znaków) poddaje się kompilacji i wykonaniu w tym samym programie, który go utworzył. W językach interpretowanych program jest wykonywany bezpośrednio po utworzeniu. Refleksja jest prostą i skuteczną metodą programowania generycznego (generic), o elastyczności i uniwersalności znacznie przekraczających poziom osiągalny przy pomocy systemów z typami polimorficznymi. Jednocześnie jest techniką dość prostą w implementacji i użyciu.
Najbardziej znanym (najwcześniejszym) językiem z refleksją jest Lisp. Pewien wariant refleksji został wykorzystany w dynamicznym SQL oraz w wielu interfejsach określanych jako języki czwartej generacji (4GL). Refleksja jest techniką programistyczną w standardzie CORBA. Zaawansowane metody refleksji wymagają istnienia odpowiednio skonstruowanej metabazy, gdzie wszelkie informacje niezbędne do składania programu z fragmentów (np. typy, interfejsy, schematy) są przechowywane w odpowiedniej postaci, np. w postaci ciągów znaków i są dobrze zestrukturalizowane. Wadami refleksji są: dość uciążliwe programowanie bardziej skomplikowanych zadań, przystosowanie do języków interpretowanych i późno wiązanych (np. jest nierealizowalna w C++ i C#), słabe możliwości w zakresie mocnej kontroli typów (co czyni ją techniką niebezpieczną). Istnieją koncepcje zdyscyplinowania refleksji tak, aby była ona typologicznie bezpieczna; jak dotąd nie wyszły one poza fazę badawczą.
12. Value semantics - semantyka oparta na wartościach, inaczej - semantyka oparta na kopiowaniu. Ogólnie, określenie semantyki pewnej konstrukcji gramatycznej, w której operuje się na kopii argumentu lub składowej tej konstrukcji. Np. iterator
for each x in K do p(x)
(gdzie x jest zmienną iteratora, K jest kontenerem obiektów, zaś p(x) jest pewnym programem zależnym od zmiennej x wykonywanym w pętli iteratora) posiada semantykę opartą na kopiowaniu, jeżeli w każdym obrocie pętli iteratora na zmienną x podstawia się kopię kolejnego obiektu z kontenera K. Możliwe są inne konstrukcje, których semantyka jest oparta na kopiowaniu, np. metoda przekazywania parametrów określana jako wołanie poprzez wartość ze zwrotem (call-by-value-return). Semantykę opartą na kopiowaniu przeciwstawia się semantyce opartej na referencjach (reference semantics).
13. Continuations - kontynuacje, pojęcie z dziedziny semantyki denotacyjnej. Chodzi o inny niż ramki na stosie sposób przechowywania informacji o stanie maszyny przed sprzed wołania funkcji. Kontynuacja zawiera informacje wystarczające, by móc kontynuować wykonywanie programu w odpowiednim środowisku po napotkaniu instrukcji powrotu z funkcji (jest to stosowane np. w Pythonie).
14. Design-by-contract - metoda zaproponowana przez Bertranda Meyera (twórcę języka Eiffel), która traktuje konstruowanie oprogramowania jako proces oparty na kontraktach pomiędzy klientem (np. wołaniem procedury) i dostawcą (np. procedurą). Metoda zakłada określenie wzajemnych zobowiązań pomiędzy klientem i dostawcą. Opiera się na definiowaniu pewnych warunków (asercji), w szczególności inwariantów (invariants), warunków wstępnych (preconditions) i warunków końcowych (postconditions).

