Rozmowa z maszyną. Część 2
W ostatnim poście pisałam o językach programowania w kontekście ich relacji z językami naturalnymi.
Wszystko po to, by finalnie dojść do wniosku, że języki programowania to nie są języki w potocznym tego słowa znaczeniu. Jest to raczej zbiór reguł, które determinują, czy napisany kod zostanie poprawnie przetłumaczony przez kompilator lub interpreter na informacje zrozumiałe dla CPU.
TL;DR: bliżej im do systemu znaków drogowych niż do ludzkiego gadania.
Jaja na serio
Sprawa nie jest jednak w żadnym stopniu oczywista. Języki programowania przejawiają naprawdę wiele podobieństw do języków naturalnych.
Przede wszystkim są tworzone przez ludzi i rozwijane przez zgromadzone wokół niego community. Ich słownictwo determinowane jest w dużej mierze przez wyrażenia języka naturalnego. Zazwyczaj angielskiego, bo jest to lingua franca branży IT, ale istnieje pokaźny zbiór języków programowania, których bazą są inne języki, ba, nawet takie, które nie są zapisywane alfabetem łacińskim.
Języki naturalne ewoluują, zmieniają się, nierzadko przekraczając granice międzypokoleniowej zrozumiałości. W językach programowania dochodzi do podobnych zjawisk: ich poszczególne wersje mogą być niezgodne z poprzednimi, tak jak w przypadku Pythona 2 i 3. Analogicznie polszczyzna Mikołaja Reja albo angielski z “The Canterbury Tales” są niemal niezrozumiałe dla współczesnych użytkowników języka. Można powiedzieć - niekompatybilna z jej zamierzchłym wariantem.
Jednak to co odróżnia języki programowania od naturalnych, to ich super poważne podejście do znaczeń słów i nabożne podejście do składni. Są one w opór dosłowne i mało podatne na jakiekolwiek odstępstwa. Sztywne jak język prawniczy - przynajmniej w założeniu pozbawiony wieloznaczności, mocno sformalizowany. Języki programowania mają bardzo prostoduszne, ewangeliczne wręcz pojęcia prawdy i fałszu.
Niechaj więc mowa wasza będzie: tak – tak, nie – nie, bo co ponadto jest, to jest od złego (Mt 5:36–37)
Ta biblijna 😈 fraza dobrze opisuje, jakich informacji oczekuje maszyna od języków wysokopoziomowych. Precyzyjnych, konkretnych, spójnych. Nie wciśniesz kitu kompilatorowi, że wartość boolowska true została przez ciebie użyta przewrotnie jako false albo że pointer wskazujący na null miał tak naprawdę miał wskazywać na coś innego. Ironia, ta bliska krewna poczucia humoru, nie jest mocną stroną maszyn. Za to całkiem sprawnie korzystają z niej programiści.
Uroczym przykładem beki toczonej przez programistów są Easter Eggs - nieudokumentowane i nieoczekiwane reakcje programu na określone działania użytkownika.
Można je znaleźć w wielu aplikacjach, z których na pewno wielu z was korzystało, jak Excel czy Word. Sporo wielkanocnych jajeczek ma w sobie Python i Linux. Ukrywają się w hardwarze, a nawet w samym silikonie. Bez jaj, trzeba być uber śmieszkiem, żeby coś takiego wymyślić.
// magic. do not touch.
Humorystyczne smaczki w kodzie pojawiają się też w komentarzach. Są to wiadomości od programistów dla innych programistów objaśniające kontekst podjętych decyzji programistycznych. Albo odwrotnie - braku decyzji albo pracy jak słynne (TODO). Komentarze czasem mogą być napradę zabawne i zachęcam do kliknięcia w tę nitkę na StackOverflow, żeby się o tym przekonać.
Programiści są tak niepoprawnymi śmieszkami, że nawet w tak poważnych przedsięwzięciach jak kod do modułu sterującego Apollo 11 (tego, który wysłał ludzi na Księżyc) czają się zabawne wstawki. Świetnie podsumował je Curious Marc i jego ziomki:
Albo inny przykład. Niedawno Microsoft wypuścił na GitHubie kod źródłowy dla BASIC 6502. Dla przypomnienia: był to napisany w asemblerze interpreter tego języka dla mikroprocesorów 8-bitowych takich jak MOS 6502. Chodziły na nich takie ikony informatycznej popkultury jak Apple II, Entertainment System (NES) czy Commodore 64. Kawał historii w jednym pliku! Dziwi mnie trochę, że nikt nie zwrócił uwagi na czyste złoto w postaci komentarzy do kodu BASIC 6502. Poniżej moje ulubione cytaty od autorów kodu - samego Billa Gatesa i Rica Weilanda 🙃.

A teraz łyżka dziegciu do tej beczki śmiechu. Komentarze nie są integralną funkcją języków programowania. Kod może się bez nich obejść. Procesor ma je kompletnie w dupie.
Rzeczywistość maszyny
Co jeszcze ma w dupie procesor? Właściwie to całą ludzką rzeczywistość.
Języki programowania pozbawione są elementu wzrokowego, głosowego i gestów towarzyszących mowie ludzi. Nie mają dostępu do całego kontekstu rzeczywistości zewnętrznej: typowo ludzkich doświadczeń i obserwacji. To pewnie dlatego nas śmieszą wyjęte z dupy odpowiedzi AI na pytania, z którymi poradziłoby sobie nawet małe dziecko.

W zasadzie nie ma w tym nic śmiesznego. Jest to dowód na to, że o odpowiedziach na nasze prompty decyduje probabilisytyka, a nie jakaś ponadprzeciętna inteligencja przypisywana maszynie. Komputer nie ma możliwości sprawdzenia, czy wyniki wykonywanych przez niego operacji mają pokrycie w rzeczywistości, w której żyją ludzie. Jeśli nasz program wypisze następujący statement o świecie:
isHot = True
print("Is it hot today?\n",isHot)
a na zewnątrz będzie pizgawica, to CPU w to nie wnika. W realności maszyny wszystko się zgadza, zdefiniowane w kodzie operacje zostały wykonane poprawnie. Nie pytamy kalkulatora o świat dla niego niedostępny, kropka. Jego odpowiedzi mogą się z nim pokrywać jedynie na drodze zbiegu okoliczności, a nie powszechnie obowiązującej reguły.
Jest to uniwersalne prawo i zarazem główna różnica między językami ludzi i maszyn. Mają punkty wspólne w zakresie struktury, ale odnoszą się do zupełnie innych rzeczywistości. Należałoby stosować do nich inne kategorie opisu, żeby nie ugrzęznąć w dwuznacznościach, którymi skażony jest dzisiejszy język pisania o komputerach, szczególnie w kontekście rewolucji AI.
Jest to może kontrowersyjne stwierdzenie, bo przecież software to twór czysto ludzki i jako taki istnieje w “naszej rzeczywistości”. Może mieć z nią punkty przecięcia: pobierać i przetwarzać dane z sensorów, skanować przestrzeń, fale dźwiękowe i świetlne. Sterować odkurzaczem, samolotem, reaktorem jądrowym i ekspresem do kawy. Nie zmienia to jednak faktu, że jego adekwatne reakcje na nieprawidłowe odczyty sensorów to kwestia jedynie dobrego error handlingu i fallbacków, a nie realnej oceny sytuacji zewnętrznej.
Tam i z powrotem
W językach naturalnych rzeczywistość, w której żyją ludzie, to podstawa dla semantyki, czyli znaczeń poszczególnych słów i wyrażeń. Kontekst zewnętrzny wypowiedzi (na przykład to, kto mówi, jakim tonem i na co wskazuje) często usuwa wieloznaczności, których pełno jest w mowie potocznej. Jeśli w knajpie powiemy: “podaj mi to” wskazując na solniczkę, nasz towarzysz zapewne bez zastanowienia sięgnie po naczynko z solą i nam je poda. A przecież “to” mogłoby być według wykładni słownikowej wszystkim.
Takie wyrażenia, które nie mają stałego znaczenia, a ich sens zależy od kontekstu wypowiedzi, mają w językoznawstwie swoją specjalną nazwę. A nawet nazwy. Są to: deiksy (tego słowa będę dalej używać), wyrażenia okazjonalne lub deiktyczne, shiftery. Służą nam do osadzania wypowiedzi w konkretnej sytuacji:
- czasowej (np. “teraz”, “potem”, “wczoraj”)
- osobowej (np. “ja”, “ty”, “ona”, “ten”)
- przestrzennej (np. “tu”, “tam”)
Występują w każdym języku naturalnym, a ich znaczenie zależy w pełni od kontekstu, w którym zostały użyte. Banalny przykład: mówiąc “chodź tu” wskazujesz na inne miejsce, kiedy wypowiadasz je z głębin swojej piwnicy, a inne, gdy przywołujesz psa na spacerze w lesie.
Na chłopski rozum deiksy nie powinny występować w językach programowania, bo wprowadzają element znaczeniowej niestabilności. Tak jednak nie jest. W kodzie znajdziemy wiele wyrażeń, których znaczenie jest definiowane przez ich kontekst wystąpienia. Przykład? Relative paths można uznać za deiksę przestrzenną:
./tutaj.txt // plik w tym tutaj katalogu
../tam.txt // plik w tym katalogu wyżej
Kod, podobnie jak wiersz czy powieść, istnieje fizycznie. Kiedyś był przecież drukowany na kartach perforowanych i taśmach. Jego obecny wariant jest może mniej namacalny, ale wciąż ma wymiar przestrzenny - zajmuje określoną powierzchnię wizualną w IDE i X przestrzeni na dysku. Wykonanie kodu wprowadza do niego jeszcze wymiar czasowy.
Z tego powodu w językach programowania występuje wiele wyrażeń przypominających deiksy, na przykład:
Podałam przykłady z różnych języków i używanych w rozmaitych kontekstach, ale zasada jest ta sama: znaczenie wszystkich tych słów jest zależne od kontekstu, w którym występują. Języki programowania nie ustępują tutaj językom naturalnym. Mają swoją rzeczywistość czasową, podmiotową i przestrzenną, do której się odwołują.
Morsy i statki kosmiczne
Używanie języka jest dla jego użytkowników intuicyjne. Od dziecka uczymy się składni i spokojnie możemy się obyć w codziennej komunikacji bez wiedzy, czym jest rzeczownik, a czym czas przyszły niedokonany. Po prostu wiemy, kiedy należy użyć określonej konstrukcji słownej. Poznawana od wczesnych lat życia składnia służy nam za rusztowanie, które składamy z pasujących do siebie elementów.
Na przykład w wielu językach fraza “nie tylko X…” pozwala przypuszczać, że pojawi się za nią informacja o tym, co jeszcze poza X. Są to tak zwane spójniki skorelowane , których jest w językach jest mnóstwo. “Nie tylko X…, lecz także Y” jest semantycznie bardzo zbliżone do operatora logicznego ‘&&’ lub ‘and’ w wielu językach programowania.
W kodzie spotkamy cały szerego podobnych konstrukcji składniowych, które miło się tłumaczy na ich odpowiedniki w językach naturalnych. Należy do nich na przykład walrus operator (:=) w Pythonie. Pozwala on na jednoczesne przypisanie wartości do zmiennej i użycie tej wartości w wyrażeniu. Zamiast pisać:
x = 5
if x > 3:
print("x jest większe od 3")
Możemy napisać zwięźle z walrusem:
if (x := 5) > 3:
print("x jest większe od 3")
Ale gdyby przetłumaczyć na to język mówiony, to otrzymalibyśmy następujące wyrażenia.
Bez walrusa:
X to liczba 5.
Jeśli X to więcej niż 3, to napisz, że X jest większe od 3.
Z walrusem użyjemy zdania podrzędnie złożonego połączonego zaimkiem względnym “który”:
Jeśli X to liczba 5, która jest większa od 3, to napisz, że X jest większe od 3.
Może efekt nie jest powalający, ale rozumiecie ideę: pewne operacje językowe możemy wykonać na różne sposoby bez utraty sensu - wystarczy zastosować inne rusztowanie składniowe.
Innym przykładem ekonomii składniowej w językach programowania jest spaceship operator (<=>), który bez zbędnych ceregieli porównuje dwie wartości i zwraca
- 0, jeśli są one równe
- 1, jeśli wartość po lewej jest większa
- -1, jeśli wartość po prawej jest większa
Wyrażenie 7 <=> 7 zwróci nam 0, bo to te same wartości.
W języku naturalnym takie porównania robimy na co dzień:
- “Zarabiam więcej Kumar” –> (1)
- “Mam na sobie tyle bugów, co Dmitro” –> (0)
- “Mam mniejszy AKUP niż Oktawian” –> (-1)
I na deser tych rozważań operator warunkowy trójargumentowy. W lakoniczny sposób przedstawia on jednocześnie warunek oraz informację, co się stanie, jeśli nie zostanie on spełniony.
int cookies = 2;
printf("%s\n", (cookies > 0) ? "Weź ciastko!"
: "Ups, skończyły się ciastka");
W tłumaczeniu na nasze: są dwa ciastka, więc weź ciastko! Chyba nie da się prościej.
I na swój sposób piękniej. Ale o tym już w następnej części.