PHP kontra reszta świata
Na PHP wieszają psy prawie wszyscy, a w szczególności ludzie, którzy bardzo mało w nim programowali lub dawno zarzucili swe więzy z tym językiem. Dlatego przed rozpoczęciem zabawy postanowiłem zebrać do kupy wszystko, co mnie w tym języku wkurza, a co jest plusem, zwłaszcza w kontekście moich ostatnich zabaw z Rubym.
Minusy:
- Mega-bałagan w bibliotece standardowej.
- Niewykorzystywanie możliwości języka przez niego samego. Dlaczego 95% rozszerzeń, nawet jeśli powstały już za czasów PHP 5, generuje błędy przy pomocy Warning, kiedy od sześciu lat są wyjątki, albo wynajduje koło od zera w tej dziedzinie?
- Naleciałości historyczne w semantyce samego języka, które trochę utrudniają pisanie.
- Jak wyżej, ale w składni.
- Wiele bardzo potrzebnych dodatków zostało dodanych niedawno, więc nie do końca działają poprawnie. Przykładem jest nowy odśmiecacz pamięci, który byłby fajny, gdyby nie wycieki pamięci oraz wykrzaczanie interpretera na tak popularnych aplikacjach, jak PHPUnit.
- Brak wielowątkowości.
- Niespójny system typów.
Plusy:
- Elegancka składnia wywodząca się z rodziny C. Przynajmniej ja osobiście uważam ją za dużo lepszą i bardziej precyzyjną, niż wynalazki w takim Rubym.
- Świetny model obiektowy - jest tylko kilka drobnych braków, które można doszlifować, lecz poza tym korzysta się z niego bardzo przyjemnie.
- Pojedyncze, dobrze zaprojektowane rozszerzenia: PHAR, PDO
- Skalowalność - dobrze sobie radzi z prostymi skrypcikami, by coś sprawdzić lub policzyć na szybko, jak i z większymi aplikacjami.
Teraz możemy przejść do zabawy w poprawianie PHP.
Zmienne...
Ze zmiennymi w PHP mamy trochę shizofreniczną sytuację. Teoretycznie można je używać od razu bez potrzeby deklarowania czy inicjacji, ale w praktyce trzeba to robić, aby nie zostać zawalonym setkami komunikatów Notice. To, czy mają się one wyświetlać czy nie, zależy od ustawień konfiguracji i widzimisię administratora i naprawdę jest ciężko, gdy trzeba skorzystać ze skryptu, którego twórca sobie sprawę olał. Dlatego podstawowa zmiana to wymuszenie zainicjowania zmiennej przed użyciem pod groźbą rzucenia wyjątku. Nie jest ważne, gdzie to robimy, jak w Javie. Po prostu w momencie, gdy odwołujemy się do zmiennej, interpreter musi mieć w pamięci informację, że jest do niej jawnie przypisana wartość.
System typów
System typów można znacząco uprościć zauważając, że nie ma żadnego technicznego uzasadnienia, dla którego tablica z haszowaniem jest "tablicą", a wszystko inne, wliczając w to pozostałe struktury danych - obiektem. W języku potrzebne są właściwie dwa typy:
- Scalar - typ prymitywny: string, integer, float, boolean
- Object - typ złożony, czyli obiekt
Nie jestem zwolennikiem wciskania obiektów w każdy najmniejszy zakamarek języka. Ma to duży sens przy rozważaniach akademickich, ale w praktyce rodzi wiele trudnych pytań, na które nie ma dobrej odpowiedzi. Przypuśćmy, że integer byłby typem obiektowym? Jak powinienem rozumieć zapis 13? Czy jest to obiekt liczbowy z wartością "13", a może obiekt "13"? Czy dwa obiekty liczbowe o wartości 13 są tym samym czy czymś innym? Jeśli obiekt liczbowy, to czym jest pojęcie "wartości", skoro w języku wszystko jest obiektem? Obiekt powinien reprezentować byt, więc dlaczego "13" nie jest obiektem reprezentującym liczbę 13 poprzedzonym przez obiekt "12", a następującym po "14"? Jak to się ma do efektów ubocznych funkcji, kiedy obiekty kopiowane są przez referencję, a nie przez wartość? I najważniejsze: na co tyle obiektów w pamięci?
System typów nie musi być ścisły, jak w Javie, stąd nie byłoby problemów z umieszczaniem wartości prymitywnych w strukturach danych ani przekazywaniem ich poprzez argumenty:
function foo($foo, scalar $bar, ClassName $joe) { // ... } // end foo();
W obrębie typu 'scalar' interpreter zachowywałby się tak samo, jak w PHP, automatycznie rzutując wartości tam, gdzie to konieczne. Jednak przejście z obiektu na typ skalarny lub na odwrót bez jawnej konwersji możliwe by nie było. Praktyka pokazuje, że jest to źródłem wielu błędów, a przy okazji złych praktyk programistycznych, jak np. funkcja("$zmienna").
Skoro już wspomnieliśmy o praktykach programistycznych, warto zastanowić się nad problemem niechlujnego pisania. Twórca Pythona postanowił leczyć nierobienie wcięć, czyniąc je integralnym elementem języka. W poprawionym PHP wyrzuciłbym interpolację ciągów tekstowych: "tekst $zmienna tekst". Nie jest ona do niczego potrzebna - dokładnie ten sam efekt można osiągnąć kropką i zwyczajnym scalaniem, a jest to nawet wydajniejsze i przynajmniej da się uniknąć takiego potworka, jak powyżej.
W obrębie typów prymitywnych wprowadzone mogłyby być następujące zastrzeżenia:
- Długość typu liczbowego zależy od platformy, jednak przy próbie jego przekroczenia liczba jest w tle konwertowana na typ "big integer", jak w Rubym. Użycie systemu 64-bitowego powinno po prostu znacząco przyspieszyć obliczenia na dużych liczbach.
- Ciągi tekstowe zakodowane są wewnętrznie w UTF-16; cały język wykorzystuje to kodowanie natywnie. Dlaczego UTF-16? Jest to kompromis między zwięzłością UTF-8 dla języków anglosaskich, a przydatnością do zapisu alfabetów nieeuropejskich. Podobne założenie przyjęte jest w Javie.
Rozszerzenia składni
Pierwsza przydatna rzecz to blok 'finally':
try { // blok krytyczny } catch(Exception $wyjatek) { // obsługa wyjątku } finally { // sprzątanie, obowiązkowe // bez względu na to czy rzucamy wyjątek czy nie }
Problem sprzątania po wyjątkach jest dość poważny. W szczególności wyskoczenie ze środka dość ważnego procesu mogłoby zostawić go w stanie niespójnym i uczynić dany kawałek kodu niezdatnym do użycia.
Wprowadzenie cech do języka, znanych pod angielską nazwą "traits", co nawiasem mówiąc już jest zaproponowane do PHP. Cecha to przydatny sposób na ominięcie braku wielodziedziczenia. Luki po nim nie łatają interfejsy. Cecha to zwyczajny zbiór metod i pól, które na etapie kompilacji są "w tle" doklejane do danej klasy, dzięki czemu możliwe jest wstrzyknięcie także implementacji. Ważną własnością cech jest ich prostota - rodzą one dużo mniej problemów niż domieszki, zwłaszcza w przypadku konfliktów nazw.
trait Foo { public function metoda() { echo 'Hello!'; } // end metoda(); } class Bar { use Foo; // kod... }
Język oczywiście posiadałby przestrzenie nazw, ale skoro nie jesteśmy ograniczeni wsteczną kompatybilnością, wykonujemy roszadę:
- do łączenia ciągów tworzymy operator ~,
- zwolnioną kropkę wykorzystujemy jako separator przestrzeni nazw.
Co więcej, dodajemy polecenie import do importowania klas. Byłaby to część zupełnie przeprojektowanego mechanizmu odnajdowania plików z kodem. Prymitywne require oraz include_path po prostu nie sprawdza się w sytuacjach, gdy różne klasy mogą mieć różne przeznaczenie. We frameworkach chcemy, aby biblioteki były elegancko dołączane z określonego katalogu, ale klasy kontrolerów i modeli rządzą się własnymi prawami. Tutaj zastosowałbym pomysł rodem z Open Power Libs. Interpreter dostaje nazwę klasy oraz przestrzeni nazw do załadowania. Bada sobie więc pierwszy element tej przestrzeni i sprawdza w katalogu, jaka ścieżka jest dla niego przewidziana oraz ew. jaka zewnętrzna ładowarka:
Loader::addPath('framework', '../lib/'); Loader::addPath('controller', '../app/frontend/controllers'); Loader::register('controller', myCustomLoader); // Ręczny import import framework.logs.Logger; // Autoładowanie $foo = new controller.Index; // Dołączanie pliku z luźno wstawionym kodem // wyłącznie ścieżki relatywne lub bezwzględne require('../plik.php');
Zaletą tego podejścia jest wydajność. include_path przy skanowaniu po kolei sprawdza wszystkie katalogi, a operacje dyskowe kosztują, zwłaszcza gdy wszystko musi przejść jeszcze przez naszą dodatkową ładowarkę. Dlaczego interpreter ma skanować wszystko, kiedy może po prostu przeczytać, o jaką przestrzeń nazw nam chodzi i od razu skoczyć do wymaganej lokalizacji?
Niedociągnięciem PHP jest brak kontroli nad elementami statycznymi klas. Nie ma ani statycznego konstruktora, ani możliwości ewentualnego wyczyszczenia stanu, co bardzo utrudnia debugowanie. Dlatego powitałbym takie rozwiązanie:
class Foo { static private $foo; static function __construct() { echo 'Konstruktor statyczny'; } // end __construct(); static function __destruct() { echo 'Destruktor statyczny'; } // end __destruct(); } // end Foo;
Do tego pojawiłby się specjalny blok 'static' kontrolujący zakres dostępności elementów statycznych. Rozpatrzmy przykład:
Proces tworzenia i niszczenia elementów statycznych nie byłby już związany z początkiem i końcem działania skryptu, ale z blokiem static. W momencie pierwszego odwołania do elementu statycznego klasy wywoływany byłby statyczny konstruktor, a przy jego opuszczaniu - destruktor. Gdy interpreter wejdzie do drugiego bloku, zainicjuje statyczną część klasy od nowa. Domyślnym blokiem static byłby cały skrypt, przez co bez korzystania z tej funkcjonalności wszystko działałoby po staremu. Za to pakiety do testowania jednostkowego mogłyby bardzo łatwo zapewnić izolację, po prostu wykonując każdy test we własnym bloku static. Byłby to też sposób na kontrolowanie życia singletonów. Jako rozszerzenie można by podawać w nawiasie, jakich klas ma dotyczyć statyczny blok: static(Klasa1, Klasa2, Klasa3){ ... }.
W obiektach doszłoby do małego uporządkowania paru drobiazgów. Najważniejszą zmianą byłoby ustalenie późnego wiązania statycznego jako domyślnego, gdyż obecnie jest tu pewna niekonsekwencja - w obiektach mamy pełen polimorfizm, a w elementach statycznych już nie. Uściślone zostałyby także niewłaściwe praktyki programistyczne.
Ostatnia ciekawa zmiana to wprowadzenie programowania kontraktowego. Chodzi o dodanie możliwości definiowania kontraktów mówiących o tym, jaki jest spodziewany stan wejściowy i jaki wyjściowy:
public function foo($argument) in { assert($argument instanceof Foo || $argument !== null); } out($result) { assert($result != null); } body { return $foo->processSomething(); }
Identyczny patent zastosowany jest w języku D, jednak tam kod kontraktów kompilowany jest na życzenie. W języku interpretowanym byłby to dodatkowy (spory) narzut wydajnościowy, dlatego należałoby najpierw pomyśleć, jak sobie z tym poradzić.
Ostatnie dodatki służyłyby do szybkiego tworzenia struktur danych:
// tablica z haszowaniem $foo = new ['foo' => 'bar', 'joe' => 'goo']; // słownik $bar = new {'foo', 'bar', 'joe'}; // tablica o stałym rozmiarze $joe = new 15:['foo', 'bar', 12 => 'joe'];
Biblioteka standardowa
Kluczem w bibliotece standardowej jest porządek i niemal pełna obiektowość. Funkcjami pozostałyby podstawowe operacje na typach prymitywnych, ale zostałyby one umieszczone w odpowiednich przestrzeniach nazw, np. math.sin() i logicznie ponazywane. Poza tym cała reszta opierałaby się na obiektach: bazy danych, XML, przetwarzanie obrazków, wyrażenia regularne i co tam jeszcze sobie wymyślimy. Wszystkie błędy raportowane jako wyjątki, co wspólne, jest konfigurowalne przez wspólne API i nie ma czegoś takiego, że każde rozszerzenie sobie wymyśla np. własne funkcje obsługi błędów.
Współbieżność
Jedną z największych bolączek PHP jest brak wsparcia dla programowania współbieżnego, co uniemożliwia praktycznie tworzenie porządnych serwerów aplikacji. Do wyboru są dwa podejścia. Pierwsze to zaimplementowanie klasycznych prymitywów takich, jak klasa Thread, semafory, monitory, zmienne warunkowe i zamki. Są one powszechne, ale na dłuższą metę ciężkie w użyciu, szczególnie w większych aplikacjach. Rozwiązaniem mogłoby być wprowadzenie alternatywnego modelu współbieżności tak, jak to zrobił Erlang, gdzie mamy procesy, które przesyłają sobie komunikaty. Myślałem trochę nad erlangowym modelem, ale jest tu pewien problem: Erlang jest językiem funkcyjnym. Nie ma w nim prawdziwych zmiennych, a tym samym i efektów ubocznych, w związku z czym przesłanie obiektu w komunikacie innemu procesowi absolutnie niczym nie grozi. W językach imperatywnych na przeszkodzie stoi właśnie możliwość zmiany wartości obiektów niespodziewanej dla drugiego wątku/procesu i uszkodzenie aplikacji.
Środowisko
Aplikacje napisane w PHP mogą pracować w różnych trybach: konsola, odpowiedź na żądanie HTTP czy aplikacja okienkowa, jednak dostanie się do informacji o aktualnym środowisku uruchomieniowym jest straszliwie pomieszane. Nikt chyba nie ma wątpliwości, że obecność zmiennej $_POST, gdy aplikacja uruchomiona jest na konsoli, nie ma najmniejszego sensu. Pomimo tego taka zmienna istnieje i ma się dobrze.
Rozwiązaniem tego byłoby wprowadzenie nowego prymitywu o nazwie Environment, po którym dziedziczyłyby szczególne rodzaje środowisk. Każdy skrypt w momencie odpalenia dostawałby obiekt aktualnego środowiska, poprzez który mógłby pobrać odpowiednie dane. Oto przykładowy pseudokod dla skryptu odpalanego na dzisiejszą modłę, gdy proces obsługuje pojedyncze żądanie HTTP:
$environment = lang.getEnvironment(); if($environment instanceof HttpRequestEnv) { echo 'Jestem uruchomiony jako generator odpowiedzi na żądanie HTTP. Dane POST: '; foreach($environment.getPost() as $name => $value) { echo $name~' => '~$value.EOL; } } if($environment instanceof ConsoleEnv) { echo 'Jestem uruchomiony jako aplikacja konsolowa.'; }
Jeszcze ciekawszy przykład zastosowania to serwer aplikacji, który można odpalić jako aplikację FastCGI i podłączyć do serwera WWW. Odpalone z niego procesy mogłyby komunikować się z procesem-matką, który zarządzałby konfiguracją, połączeniami z bazą itd. dzięki czemu nie trzeba by się tym zajmować w kółko za każdym razem:
$env = lang.getEnvironment(); if(!$env instanceof FastCGIEnv) { throw new EnvironmentException('Nieprawidłowe środowisko'); } import appserver.Request; while($message = $env->dispatchMessage()) { switch($message) { instanceof fastcgi.Init: appserver.initialize(); break; instanceof fastcgi.Request: thread.spawn(appserver.Request.run, appserver.prepareProcessData(), $message->getRequest()); break; instanceof fastcgi.Done: appserver.done(); } }
Przy okazji widzimy tu ciekawe rozszerzenie możliwości instrukcji switch, nad którym także można by się zastanowić.
Interpreter
Ostatnie propozycje to przede wszystkim wbudowany od początku odśmiecacz pamięci oraz wbudowane automatyczne kompilowanie do bajtkodu, które zrzuca obraz do pamięci lub na dysk, by nie robić tego po sto razy.
Zakończenie
Mam nadzieję, że wpis był inspirujący i zachęcił do pomyślenia nad kształtem PHP teraz i w przyszłości, a zaprezentowane tutaj kierunki rozwoju się Wam spodobały.






Napisał Zyx w poniedziałek, 1 lutego 2010 o 17:34
WebCM -> udostępnia, nazywa się PHP_EOL i zawiera znak nowej linii charakterystyczny dla systemu operacyjnego, na którym uruchomiony jest interpreter.
bigZbig -> problem z polami statycznymi polega na tym, że jeśli są one prywatne, to o ile twórca klasy tego nie przewidział, za Chiny ich nie zresetujesz. Dopiero w PHP 5.3 można przy pomocy długiego kombinowania i zabaw z Reflection API dostać się z zewnątrz do prywatnego pola obiektu, ale bynajmniej nie jest to ani wygodne, ani przyjemne.
A o pomysłach wprowadzenia cech do PHP wiem - zamieściłem info o nich dlatego, że jak najbardziej je popieram.