Perl: językowy cudak o wielkiej mocy

Perl jest ważnym językiem w historii programowania. Odegrał ważną rolę w historii internetu, bo przez przed długi czas był podstawowym narzędziem do tworzenia dynamicznych stron WWW. Ktoś mógłby powiedzieć, że było, minęło. Po co mi to?

Myślę, że tym językiem warto zainteresować się nie koniecznie po to, żeby w nim programować (choć można, obecnie programuję zawodowo w Perlu i jestem z tego zadowolony), a po to żeby poszerzyć swoje wyobrażenia, jak może wyglądać język programowania i jakie narzędzia do budowania abstrakcji może nam zapewniać.

Od dłuższego czasu interesuję się tworzeniem języków programowania (jeden nawet stworzyłem na potrzeby kursu podstaw programowania na tej stronie) i pod tym względem Perl jest bardzo inspirujący. Będę omawiać wersję piątą, obecnie wiodącą, choć w przygotowaniu jest wersja 6 ze sporymi zmianami.

Spis treści

  1. Perl — kontynuator *nixowej tradycji
  2. Egzotyczna składnia
  3. Podsumowanie

Perl — kontynuator *nixowej tradycji

Myślę, że dla zrozumienia Perla, kluczowym jest wiedzieć skąd się wywodzi. Mianowicie stanowi kolejny krok na ścieżce wyznaczanej przez starsze języki typowe dla *nixów, które już opisałem wcześniej: języka powłoki i AWK. Jest zatem dostosowany do podobnego środowiska, w którym łączymy strumieniami programy, które przetwarzają dane, przeważnie tekstowe, w których linia tekstu jest główną jednostką miary i przeważnie linijka stanowi zapis pojedynczej informacji. Na przykład:

[lew@T430 wiedz.net.pl]$ ls -l /tmp
razem 52
drwxr-x--T 2 lew  lew  4096 maj  7 18:40 dumps
prw-r----- 1 lew  lew     0 maj  9 13:17 dwm.cmd
prw-r----- 1 lew  lew     0 maj  9 13:59 dwm.in
prw-r----- 1 lew  lew     0 maj  5 20:47 dwm.out
-rw-r--r-- 1 lew  lew   866 maj  9 04:55 gameoverlayui.log
-rw-r--r-- 1 lew  lew   766 maj  8 18:31 gameoverlayui.log.last
drwx------ 2 root root 4096 maj  5 20:46 pulse-PKdhtXMmr18n
drwx------ 2 lew  lew  4096 maj  5 20:47 ssh-qENuT5ikEE4q
srwxr-xr-x 1 lew  lew     0 maj  7 18:40 steam_chrome_overlay_uid1000_spid19
331
srwxr-xr-x 1 lew  lew     0 maj  8 13:02 steam_chrome_overlay_uid1000_spid26
430
srwxr-xr-x 1 lew  lew     0 maj  8 16:41 steam_chrome_overlay_uid1000_spid28
216
srwxr-xr-x 1 lew  lew     0 maj  8 20:17 steam_chrome_overlay_uid1000_spid30
546
srwxr-xr-x 1 lew  lew     0 maj  8 13:02 steam_chrome_shmem_uid1000_spid2623
5
drwx------ 3 root root 4096 maj  5 20:46 systemd-private-4df9d0e913c04ecf8ce
abd33a622c2a7-apache2.service-uLn1HU
drwx------ 3 root root 4096 maj  5 20:46 systemd-private-4df9d0e913c04ecf8ce
abd33a622c2a7-ModemManager.service-6wjCoG
drwx------ 3 root root 4096 maj  5 20:47 systemd-private-4df9d0e913c04ecf8ce
abd33a622c2a7-rtkit-daemon.service-C1wTtt
drwx------ 3 root root 4096 maj  5 20:46 systemd-private-4df9d0e913c04ecf8ce
abd33a622c2a7-systemd-timesyncd.service-0YsWVA
drwx------ 3 root root 4096 maj  5 20:46 systemd-private-4df9d0e913c04ecf8ce
abd33a622c2a7-upower.service-KKIXn9
drwx------ 2 lew  lew  4096 maj  6 08:44 Temp-68710c4c-d660-48ea-afce-fd6670
b6a672
drwx------ 2 lew  lew  4096 maj  9 12:02 Temp-7f98b19e-388b-40bc-9c60-428b6b
7c6b06
drwx------ 3 lew  lew  4096 maj  9 12:03 Temp-9e5b59a4-5764-4229-ac4e-a3da26
1f41c5

tu jedna linijka to jeden plik, dla polecenia ps to będzie jeden proces. Ta sama zasada dotyczy również nagłówka HTTP:

HTTP/1.1 200 OK
Date: Sun, 09 May 2021 12:08:50 GMT
Server: Apache/2
Upgrade: h2,h2c
Connection: Upgrade
Last-Modified: Sun, 03 Jan 2021 14:11:52 GMT
ETag: "1370-5b7ff8e177a00"
Accept-Ranges: bytes
Content-Length: 4976
Vary: Accept-Encoding,User-Agent
Content-Type: text/html

Dotąd język powłoki służył do spajania ze sobą poleceń; AWK łączył w sobie moce grepa i seda, pozwalając pisać skrypty, które wymagałyby wielokrotnych uruchomień tych dwóch. Jednak, pomimo, że miał nawet funkcje i tablice asocjacyjne, napisanie w nim bardziej złożonego programu jest kłopotliwe.

W tym miejscu wkracza Perl, który łączy oba w pełnoprawny język programowania ogólnego przeznaczenia. Co więcej, język, któremu nawet jak na dzisiejsze standardy, zasadniczo niczego nie brakuje. Posiada wyrażenia regularne ładnie wbudowane w język, przy których implementacje z innych, współczesnych języków są bardzo okrojone i nieporęczne. Pomijam to, że w niektórych językach trzeba pisać wyrażenia regularne jako zwykłe łańcuchy znaków i podwajać każdy ukośnik, bo np. Python i Ruby mają na to rozwiązanie.

Chodzi mi o zwięzłość taką jak w tym kawałku wyodrębniającym wszystkie tagi script z kodu HTML do tablicy @scripts:

my @scripts = ($html =~ /<script[^>]+>/g);

Idąc dalej mogę napisać tak, żeby wybrać tylko nazwy plików z załączonymi skryptami:

my @scripts = map { /href="([^"]+)"/; $1 }
                  ($html =~ /<script[^>]+href=[^>]+>/g);

Właściwie to nie powinienem rozróżniać wielkich i małych liter. Żaden problem:

my @scripts = map { /href="([^"]+)"/i; $1 }
                  ($html =~ /<script[^>]+href=[^>]+>/gi);

Żeby wyodrębnić style można napisać:

my $css = join("", ($html =~ m#<style[^>]*>([^<]+)</style\s*>#g));
my %css = ($css =~ m/(\S.*)\{\s*([^}]+)\s*}/g);

W pierwszej linijce wyodrębniam style w postaci tekstu, w drugiej od razu tworzę tablicę asocjacyjną, gdzie selektor jest kluczem, a style wartością. Jest to przykład niedoskonały, bo w przypadku gdy mamy kilka reguł dla takiego samego selektora, definicje nie połączą się, lecz uzyskamy tylko ostatnią. Chciałem jednak pokazać zwięzłość jaką niekiedy możemy uzyskać. Z resztą, poprawny kod będzie niewiele mniej zwięzły i elegancki:

my @css = ($css =~ m/(\S.*)\{\s*([^}]+)\s*}/g);
my %css = ();
while(@css) {
    (my $key, my $value, @css) = @_;

    if(defined($css{$key})) {
        $css{$key} .= $value;
    } else {
        $css{$key} = $value;
    }
}

chociaż, po wyodrębnieniu funkcji i tak jest całkiem dobrze:

sub groupHash {
    my %ans = ();
    while(@_) {
        (my $key, my $value, @_) = @_;

        if(defined($ans{$key})) {
            $ans{$key} .= $value;
        } else {
            $ans{$key} = $value;
        }
    }

    return %ans;
}

my %css = groupHash($css =~ m/(\S.*)\{\s*([^}]+)\s*}/g);

Jeśli chodzi o definiowane funkcji jest dobrze, bo Perl dobrze sobie radzi z funkcjami w parametrach. Dlatego jako język ogólnego przeznaczenia radzi sobie dobrze, pomimo kilku pułapek. Z resztą biorąc pod uwagę czasy, jak pierwszy raz programowałem zawodowo w Perlu, w 2012, funkcje anonimowe nie były bardzo popularne, a ja z przyjemnością ich wtedy używałem, bo bardzo ułatwiają tworzenie ładnych abstrakcji.

Z drugiej strony, Perl nie powstał z myślą o współczesnych potrzebach. Teraz mamy do przetwarzana JSONy, XMLe (też kochacie, XSL, prawda? ;)) i inne dane o znacznie bardziej złożonej strukturze. Robienie takich rzeczy w czystym Perlu nie należy do przyjemnych, bo mają znacznie bardziej złożoną strukturę. I te wyrażenia regularne… trochę trącą myszką.

Oczywiście, mamy biblioteki, które to robią. Z drugiej strony, tak to już jest, że taka czy inna technologia otwiera wielkie możliwości, ale będziemy zupełnie nieświadomi tego, póki nie spróbujemy. Mam zamiar niedługo napisać o pomyśle w tej kwestii, na jaki ostatnio wpadłem.

Egzotyczna składnia

Jeśli nie macie doświadczenia z Perlem, kod powyżej może wyglądać egzotycznie. Jeżeli nie używaliście wyrażeń regularnych, już w ogóle (to temat na osobny artykuł).

Typy danych, konteksty

Od razu możemy zauważyć, że zmienne mają znaczniki, podobnie jak w skryptach powłoki i PHP, tyle, że w perlu mamy aż trzy rodzaje: $ — skalar (liczba, ciąg znaków, strumień, referencja), @ — tablica i % — słownik (a.k.a hash, tablica asocjacyjna) i każdy typ ma osobną przestrzeń nazw. Jak widzieliście, nie ma problemu, żeby mieć jednocześnie $css, @css i %css.

Przy zmiennych przeważnie mamy słowo kluczowe my, które oznacza, że zmienna jest lokalna, bo domyślnie wszystkie są globalne, jedna z pozostałości po starych czasach. Taki smrodek. Pół biedy, bo opcja use strict; pozwala nam wyłapywać, między innymi, zmienne globalne.

Co więcej funkcja może wykryć jakiej wartości oczekuje wywołujący (fachowa nazwa: kontekst skalarny i kontekst tablicowy) i odpowiednio dostosować wynik. Na przykład:

open(my $in, "ps -A |") or die ("can't run 'ps'");
while(my $line = <$in>) {
    #...
}

W tym kontekście, <$in> oznacza, odczytaj linijkę ze strumienia (w tym przypadku wynik polecenia ps -A), ale w tym:

my @ps = <$in>;

Zwróci już całą tablicę kolejnych linijek. Z kolei jeśli użyjemy @ps w kontekście skalarnym, np:

while(@ps) {
    #...
}

będzie on liczbą oznaczającą długość tej tablicy. Jako, że 0 oznacza wartość fałszywą, a inna liczba prawdę, taka pętla będzie działać tak długo jak tablica nie jest pusta.

Dziwne te tablice i słowniki

To jak zachowują się tablice i słowniki jest bardzo dziwne, na pierwszy rzut oka, ale w praktyce jest całkiem poręczne.

my @tab = (1,2,3);

to oczywiście prosta definicja tablicy. Co zatem znaczy to?

my @b = (@tab, 4, 5, 6);

Wcale nie drzewo, tylko tablica: (1, 2, 3, 4, 5, 6). Żeby zrobić drzewo trzeba użyć referencji:

my @b = (\@tab, 4, 5, 6); # lub używając literału:
my @b = ([1,2,3], 4, 5, 6);

Żeby było zabawniej, tablica i słownik to prawie to samo, różnią się tym jak pobieramy z nich elementy, ale nie ma problemu stosować je wymiennie:

my %h = ( a => 1, b => 2, c => 3 );
my @t = (%h, "d", 4);
%h = @t;

tablica @t ma wartość ("a", 1, "b", 2, "c", 3, "d", 4), a $h{d} (wartość dla klucza d) to 4. Natomiast operator => jest prawie tym samym co przecinek, tylko że pozwala podać nazwę (tzw. bareword) jako wartość klucza, będzie ona przekonwertowana na łańcuch znaków. Podobnie możemy zrobić pobierając wartość ze słownika.

Wygląda strasznie i napsuło krwi niejednemu początkującemu, jednak często jest to bardzo wygodne, zwłaszcza budując słowniki. Dajmy na to, że interpretujemy jakąś konfigurację:

my %conf = map { /^\s*(\S+)\s*=\s*(.*)$/;
                 (defined $1)?
                    ($1, $2)
                    :() }
               <$config>;

Dla każdej linijki próbuję dopasować dane wyrażenie regularne. Coś - równa się - coś. Cosie są w nawiasach, więc są wyodrębniane do zmiennych pomocniczych $1, $2, itd. Nie ma problemu, żebym zwrócił tablicę dwóch wartości, bo i tak zostanie dopisana do wyniku, nie zrobi się drzewo. Tak samo, jeśli dopasowanie nie uda się $1 będzie niezdefiniowane i zwracam pustą tablicę, a tym samym żaden klucz nie zostanie dodany.

Nigdy nie zdarzyło wam się mapując zwrócić kilka elementów lub żaden? Bo mi się zdarzyło nie raz i w Perlu nie mam z tym problemów. Trzeba tylko uważać jak się przekazuje tablice do funkcji, bo w tym wypadku jest tak samo.

Parametr domyślny

Pewnie zauważyliście, że wpisuję sobie wyrażenie regularne nie stosując do żadnej zmiennej. To dlatego, że Perl ma specjalną zmienną globalną, która staje się parametrem, jeśli żaden nie został podany. Nie jestem miłośnikiem takiego rozwiązania i unikam tego, jednak w przypadku funkcji takich jak map i grep i wszelakich funkcji wykonującej jakiś kod dla każdego elementu jakiegoś zbioru. Wtedy jest to, moim zdaniem uzasadnione, jest jasne i czytelne. Tym bardziej, że przyjmowanie parametrów w Perlu jest, hmm…

Parametry funkcji

Zasadniczo, funkcje w perlu przyjmują jeden parametr — tablice, która nazywa się @_. Są wyjątki, bo np. parametry funkcji grep nie są oddzielone przecinkiem, samemu też można definiować takie funkcje, ale nie jest to polecane i nie dziwię się, bo pisanie ich bywa kłopotliwe. Poza tym, przeważnie parametry ładuje się tak:

sub foo {
    my ($a, $b) = @_;
    # ...
}

Perl jest na tyle mądry, że pozwala od razu rozdysponować elementy tablicy do zmiennych. Nie ma też problemu żeby na końcu dodać tablicę lub słownik, która przechowa nadmiarowe parametry. Ostatnio szczególnie upodobałem sobie tę drugą opcję, w ten sposób mogę definiować sobie opcjonalne (lub i obowiązkowe) parametry nazwane. Jest to bardzo użyteczne gdy jest wiele szczególików jakiegoś działania, które można doprecyzować lub nie. Jest to na pewno też bardziej estetyczne niż tworzenie łańcucha wywołań różnych metod na obiekcie, jak to zwykle robi się w Javie.

Podsumowanie

Łatwo mi pisać o blaskach Perla gdy znam go bardzo dobrze, bo wiem czego robić nie należy. Wspominałem już o kilku pułapkach, inne przemilczałem. Dla początkującego nie jest łatwo, nie można też ukryć, że ten język to jedna wielka prowizorka. Jest pełen brzydkich rozwiązań, ale jak umie się go używać działa nadspodziewanie dobrze. Jest chyba, mimo wszystko, moim drugim ulubionym językiem programowania. W dziedzinie jego zastosowania nie ma właściwie poważnej konkurencji? Pythona nie lubię, Ruby też mnie nie oczarował, że o PHP nie wspomnę (tym bardziej, że ten ostatni ma znacznie węższe zastosowanie).

Perl jest zupełnie jak język powłoki. Jest do bani, archaiczny, wszystko należałoby zrobić inaczej, porządnie. Jednak w swoim zastosowaniu, pomimo usilnych prób, nikt nie stworzył nic lepszego. Tak jakby przy próbach znalezienia ładnych, poprawnych rozwiązań, zawsze umykała jakaś ważna cecha tych języków, których potem brakuje.

Z drugiej strony, czy znaczy to, że nie należy próbować? Chyba nie. W takich poszukiwaniach powstaje coś nowego, często użytecznego, choć po prostu innego, przydanego w innych przypadkach. Dużo w tej kwestii zainspirował mnie Lisp. Mój wymarzony język łączyłby cechy obu. W szczególności na sposób Lispa implementował to co robi się obecnie przy użyciu tych odrażających wyrażeń regularnych. O tym będę pisał niebawem, wraz z postępami prac, lecz jeszcze wcześniej, chcę napisać o jeszcze jednym odrażającym narzędziu, które też jest bardzo użyteczne i stanowiło dużą część inspiracji w poszukiwaniach.

Zagadka, cóż to może być? :)