Rozmowa z maszyną. Część 3
To będzie ostatni wpis z serii językowej. Na ten moment nie jestem w stanie podejść bliżej istoty problemu, który od lat męczy i jednocześnie inspiruje. Wciąż wiele jego aspektów pozostaje dla mnie tajemnicą - tak jak ogromną tajemnicą jest wciąż to, skąd w ogóle wziął się język i jakim fenomenalnym zjawiskiem jest komunikacja, nieograniczona wyłącznie do tej między ludźmi.
Programowanie jako sztuka
Nie chcę popadać w patos i nazywać programowania sztuką. Wystarczy, że zrobił to Donald Knuth. Sztuką można nazwać rzeźbę Dawida, jak i gówno w puszce, więc no… Kod jednak bezdyskusyjnie jest formą twórczości, zbudowanej na słowie pisanym. Do tej jego właściwości odwołuje się ruch Code as Speech, którego dalekim echem jest to, że niektórzy z nas mogą rozliczać AKUP.
Gwoli wyjaśnienia: „Code as speech” to hasło z amerykańskiej debaty prawnej. Jego postulaty można streścić następująco. Kod to forma indywidualnej ekspresji słownej gwarantowanej przez wolność słowa. Przenosząc to na warunki USA - przez Pierwszą Poprawkę do Konstytucji.
Takie postawienie sprawy ma kapitalne konsekwencje nie tylko dla twórców oprogramowania. Przypomnę, że jeszcze w latach 90. silna kryptografia była traktowana jako rodzaj uzbrojenia (Munitions), której eksport był kontrolowany i ograniczany przez rząd USA. Dopiero programista Daniel Bernstein, którego uważa się za twórcę ruchu “Code as Speech” w batalii prawnej wywalczył możliwość publikacji kodu do swojego systemu szyfrującego Snuffle. Podczas rozprawy argumentował, że jego program stanowi wypowiedź naukową. Sąd przyznał mu rację, a sam Bernstein został okrzyknięty pogromcą cenzury w softwarze.

Free Speech Flag. Pasek w określonym odcieniu odpowiada 3 liczbom heksadecymalnym dla formatów RGB. Ciąg hexów tworzy klucz kryptograficzny, który pozwalał użytkownikom na kopiowanie płyt HD DVD i Blu-ray.
Jeśli potraktujemy kod jako wypowiedź, to w wolnym kraju, za jaki uważają się Stany Zjednoczone a za nimi inne demokratyczne państwa, żaden człowiek i żadna instytucja nie może nam zakazać jego publikacji. Niby fajnie, ale jest jedno “ale”. Oznacza to, że programiści mogą do woli publikować swoją “twórczość”, która nie zawsze ma szlachetne zastosowanie.
Decyzja, żeby traktować kod jako wyraz indywidualnej ekspresji, obarczona jest wszystkimi zaletami i wadami wolności słowa. Z jednej strony mamy więc nieograniczoną możliwość tworzenia i rozwijania kodu jako open source. Z drugiej strony - równie nieograniczone pole do publikacji exploitów, modeli deep fake, plików CAD do druku broni 3D i całego asortymentu innych gówien.
Boilerplate i wata słowna
W ruchu Code as Speech samo porównanie kodu do mowy czy wypowiedzi (speech) raczej nie jest fortunne, bo istotą kodu jest jego materialność. Bliżej mu do jakiejś odmiany twórczości pisanej, jakiejś wręcz literatury. Tylko takiej trochę wybrakowanej. W budulcu kodu - w samych językach programowania - nie znajdziemy subtelności, werbalnego puszczenia oka do odbiorcy, ironii czy sarkazmu właściwych komunikacji między ludźmi. Nawet jeśli te chwyty występują w komentarzach czy nazwach funkcji, to dla maszyny są kompletnie przezroczyste i nieistotne. W mowie ludzi wszelkiego rodzaju ornamenty słowne i środki stylistyczne są wszechobecne.
Języki programowania są także znacznie bardziej oszczędne w słowach, szczególnie gdy programista wziął sobie do serca zasadę DRY i umie ją stosować. W wielu językach występuje jednak całkiem sporo waty słownej znanej także pod nazwą boilerplate codu.
Nazwa wywodzi się z branży poligraficznej, w której “boilerplate” oznaczał płyty drukarskie do druku powtarzalnych treści, np. reklam. Płyty były wykonane z walcowanej stali, z której wykonywane były również kotły (boilery).
Boilerplate code, chociaż nie zawsze konieczny, jest często niezbędny w takich językach jak Java czy C# - silnie typowanych i obiektowych. Co ważne, struktura boilerplate jest zabetonowana i nie wykazuje żadnych wariacji. U ludzi, szczególnie u osób sobie bliskich, wygląda to inaczej. Dyskusje przypominają najebaną ciotkę na weselu: opowiadają wciąż te same historie na wiele sposobów, często zmieniając ich treść nie do poznania w kolejnej odsłonie tej samej opowieści.
W naszych codziennych rozmowach pojawia się wiele powtórzeń, żartów, mlasków, beków, dygresji i anegdotek. Ogólnie mówiąc: dużo rzeczy, które może nie niosą znaczących treści, ale pomagają ludziom poznawać się, socjalizować, kłócić - po prostu spędzać ze sobą czas. Siłą rzeczy ten aspekt jest kompletnie obcy maszynom. I nie dajcie się zwieść rozdyskutowanym ze sobą agentom AI.
W językach naturalnych występują ponadto inne ciekawe zjawiska takie jak:
- neologizmy
- synonimy
- metafory
- rymy, rytmy
- onomatopeje
- przekleństwa
Chociaż pochodzą z różnych bajek, to łączy je jedno - nie mają swoich odpowiedników w językach programowania. Neologizmów od biedy można się doszukać w nazwach zmiennych czy funkcji. Onomatopeje, rymy - zapomnij, po co to maszynie. Przekleństwa? Znowu, tylko w komentarzach (podobno kod z bluzgami jest statystycznie lepszy). Przykłady można mnożyć.
Odwołuję się do tych zjawisk, żeby trochę pocisnąć z ruchu Code as Speech. Chcę wykazać, że w kodzie nie występują zasadnicze składniki, które zamieniają przypadkowy potok słów w pełnoprawną wypowiedź, werbalną artykulację myśli. I chociaż uważam, że z programowania jest taka speech jak z koziej pizdy gitara, to gorąco wspieram postulaty tego ruchu. W sporze między cenzurą a wolnością wolę jednak stać bliżej wolności.
Native speakers
Języki programowania, analogicznie do języków naturalnych, można podzielić na różne typy czy rodzaje. W językach naturalnych podstawą podziałów jest zazwyczaj ich pochodzenie. Język polski wywodzi się z hipotetycznego praindoeuropejskiego, który ewoluował w różnych przestrzeniach globu w osobne rodziny językowe. Obecnie zdolność porozumienia między ich użytkownikami jest mocno utrudniona bez wcześniejszej nauki języka. Przykładem jest komunikacja między Niemcem i obywatelem USA - mówią oni wzajemnie niezrozumiałymi dla siebie językami, które wywodzą się z tego samego prajęzyka.
W językach programowania podziałów jest więcej, a ich genealogia nie jest kluczową sprawą. Dzielimy je ze względu na:
- Poziom abstrakcji
języki niskiego poziomu (c, Assembler) versus wysokiego poziomu (Python, Java, JavaScript)
- Paradygmat
Programowanie Obiektowe (OOP) versus Funkcyjne czasem vs Proceduralne
- Sposób wykonania
kompilowane versus interpretowane versus hybrydy
- Typowanie
statyczne versus dynamiczne
Kiedy opisujemy język programowania musimy zdefiniować go w oparciu o wyżej wymienione kryteria. Jak ta teoria ma się do języków naturalnych? Już tłumaczę.
Tak jak native Hiszpan prowdopodobnie nic nie zrozumie z rozmowy z Japończykiem, tak kod napisany w Rust nie zostanie poprawnie uruchomiony przez interpreter Pythona. Wynika to z prostej przyczyny - interpreter Pythona rozumie wyłącznie składnię Pythona. Nie jest w stanie zrozumieć kodu napisanego w innym języku bez pośrednictwa jakiejś warstwy translacyjnej.
Z drugiej strony, każdy program w określonym języku programowania można przetłumaczyć na inny język programowania bezstratnie - tak długo, jak oba języki są kompletne w sensie Turinga i nie zostały skompilowane. Teoretycznie można by go przetłumaczyć na ciąg instrukcji x86 mov albo karciankę “Magic: The Gathering”, które według wszelkich świadectw są Turing complete. Oczywiście nie będzie to kod wybitnie wydajny :)
Inaczej ma się sprawa z tłumaczeniem kodu skompilowanego. Kompilacja to proces stratny. Oznacza to, że skompilowanego kodu nie da się automatycznie przywrócić do jego źródłowej postaci w języku wyższego poziomu. Na tym polega cały problem z reverse engineeringiem. Można to porównać do próby wydobycia pojedynczych składników z upieczonego już ciasta.
Jak wygląda tłumaczenie między językami naturalnymi? Temat-rzeka. Nie wchodząc w teoretyczne niuanse można stwierdzić, że każdy język naturalny może być w jakimś stopniu przełożony na inny język naturalny. Nie oznacza to, że wynikiem tego działania będzie dokładna translacja. Będzie jednak wystarczająca, by ludzie wywodzący się z różnych kultur i władających różnymi językami, mogli się porozumieć. Wbrew skrajnym relatywistom językowym, twierdzę, że podstawowe porozumienie jest możliwe nawet między użytkownikami bardzo odległych genetycznie i kulturowo języków.
Words, words, words
LORD POLONIUS
(…)
What do you read, my lord?
HAMLET
Words, words, words.
Koniec końców, wszystkie języki składają się ze słów - elementów niosących znaczenie. Słowo niekoniecznie musi być pisane - tak jest na przykład w języku migowym. Istotne jest to, słowo rozumiane jako jednostka znaczeniowa podlega definicjom. Jest wiele słów, które wyglądają i brzmią tak samo, ale mogą mieć różne znaczenia (homonimy). Czasem znaczenie może zależeć od kontekstu ich wystąpienia (pamiętacie deiksy z poprzedniego posta?).
Co ciekawe, słowo to również jednostka rozumienia dla procesora. Rozmiar słowa maszynowego wskazuje na to, ile bitów informacji procesor w danej architekturze może na raz przetworzyć. Zazwyczaj to 32- lub 64-bity, ale w przeszłości słowo mogło mieć arbitralną długość. Zresztą podobnie jak bajt, którego rozmiar ustandaryzował się jako 8 bitów dopiero w latach 60., wraz z wejściem na rynek komputerów IBM System/360, które ustaliły istniejący do dziś standard rynkowy.
W mowie używamy zmian tonu, tembru głosu, pauz. Dzięki nim odróżniamy, kiedy mówimy ironicznie, kiedy o coś pytamy, kiedy się wkurzamy. W języku pisanym odzwierciedla to interpunkcja. Zazwyczaj pełni ona rolę dodatku do słowa pisanego, który ułatwia odbiorcom jego zrozumienie. Użytkownicy języka często są w stanie zrozumieć wypowiedź pisemną bez znaków interpunkcyjnych, chociaż zdarza się, że mogę one zdeterminować sens wypowiedzi. Mieli się o tym przekonać kierowcy zatrudnieni w pewnej mleczarni. Brak przecinka w jednym z przepisów prawa pracy skutkował tym, że mleczarnia musiała im wypłacić 5 mln dolarów za nadgodziny.
W programowaniu symbole, które odpowiadają znakom interpunkcyjnym z języka pisanego, są integralnym elementem języka. Ich użycie jest jasno zdefiniowane na przykład:
- Przecinek zazwyczaj oddziela argumenty funkcji albo elementu w tablicy
- Wcięcie w Pythonie definiuje blok kodu
- Średniki w wielu popularnych językach oznaczają koniec instrukcji.
Znaki interpunkcyjne są kluczowe dla kompilatorów, których fundamentem jest gramatyka bezkontekstowa, a efektem ich działania - poprawne tłumaczenia języka wyższego poziomu na język zrozumiały przez procesor. Kompilatory alergicznie reagują na jakiekolwiek dwuznaczności. Walą błędami na prawo i lewo jak tylko przecinek pojawi się w nieodpowiednim miejscu, a nawias nieprawidłowo zamknięty. Laurie Kirk zrobiła o tym rewelacyjny materiał, do którego obejrzenia zachęcam:
Upodobanie kompilatorów do jednoznaczności wyklucza więc wszelkiego rodzaju subtelności, które destabilizują znaczenie słów. Zalicza się do nich wspomniana już ironia, sarkazm czy metafora, maglowana przeze mnie już od tego posta. Czy na pewno?
Finalnie nie mogę się oprzeć pokusie stwierdzenia że właśnie metafora leżała u podstaw doboru wielu konstruktów słownych w językach programowania. Czym innym “void” - funkcja nie zwracająca żadnej wartości po wykonaniu? Nieco złowieszcza “die” w PHP? Funkcja “panic” w Golang? Modyfikator “sealed” w C#? “Yield” w Pythonie? Mam wrażenie, że tymi pytaniami wracamy do punktu, od którego zaczęliśmy te rozważania - do znaków drogowych. To też są przecież słowa, słowa, słowa.
