Wczorajszego wieczora otrzymałem elektroniczny list, w którym jeden z odwiedzających zadał mi parę pytań po lekturze wpisu dotyczącego parsowania numerów telefonicznych w Clojure. Pytanie było pozornie proste i związane z technicznym detalem budowania oprogramowania, ale tak naprawdę czytelnik poruszył bardzo istotny temat, nad którym sporo myślałem kilka lat temu, a teraz postanowiłem go streścić.
Na wstępie chciałbym zaznaczyć, że używam słowa referencja w dosyć ogólnym znaczeniu, które wykracza poza mechanizm referencji znany z języków programowania, chociaż skorzystam przede wszystkim z technicznych porównań do zademonstrowania na przykładach jej istoty.
Etymologia
Czym jest referencja? Odwołam się (sic!) do serwisu Online Etymology Dictionary, gdzie możemy wyczytać, że słówko pochodzi z języka francuskiego, w którym zapożyczono je z łaciny, zaś od roku 1580 jest używane do określania czynności odwoływania się do czegoś lub kogoś. Nieco później (około roku 1602) termin ten zaczął również być stosowany w znaczeniu polecania kogoś (np. na jakieś stanowisko zawodowe).
Łacińskie słowo referre jest złożeniem przedrostka re-, który komunikuje powtórzenie czynności lub czynność będącą odpowiedzią na coś (następstwem czegoś), oraz słowa ferre oznaczającego utrzymywanie, niesienie bądź znoszenie czegoś lub kogoś.
Gdy spojrzymy na polskie synonimy referencji, z łatwością zauważymy, że mamy w naszym ojczystym języku termin o podobnej konstrukcji: odnosić (coś lub kogoś do czegoś lub kogoś). Określenie informuje więc o istnieniu pewnej relacji między dwoma lub więcej bytami, z których pierwszy wskazuje na drugi (lub pozostałe).
Warto wspomnieć, że w języku praindoeuropejskim znajdziemy rdzeń bher- (widoczny w łacinie jako fer-), komunikujący opiekę, wychowawstwo lub zwierzchnictwo. Mamy więc trochę informacji o charakterze wspomnianej relacji: zawarty jest w niej jakiś autorytet bądź inne źródło prawdy lub reguły. Przez użycie wspomnianego przedrostka re- dookreślamy tę relację, kierując ją w stronę owego źródła.
Widzimy, że wspomniane wyżej „wskazywanie” czy „odnoszenie się” nie niosą wyłącznie informacji o nawiązywaniu do czegoś, ale komunikują też zależność względem drugiej strony, którą możemy po polsku określić poleganiem (na kimś lub czymś) bądź odwoływaniem się (do kogoś lub czegoś, co jest arbitralne).
Referencją będę więc nazywał odniesienie się osoby bądź przedmiotu do innej osoby bądź przedmiotu przy relacji nacechowanej poleganiem na osobie lub przedmiocie jako źródle istotnej, rozstrzygającej wartości.
Referencja jako narzędzie
W codziennym życiu mamy całkiem sporo przykładów odnoszenia się do czegoś, na przykład w publikacjach, recenzjach, opisach, a nawet rozmowach. Nawet komentarze w serwisach społecznościowych często zawierają referencje do wypowiedzi autorytetów lub naukowych publikacji, aby racjonalizować słuszność zawieranych tez.
Spróbujmy spojrzeć na referencję w nieco utylitarny sposób, pomijając przypadki odnoszenia się przez autorów do źródeł wiedzy. Na przykład w większości języków programowania mamy mechanizm zwany właśnie referencją bądź wskaźnikiem, który służy temu, aby odwoływać się do wartości bądź operacji niebezpośrednio, lecz z użyciem stworzonej w tym celu tożsamości.
Tożsamości?! Dalej mówimy o komputerach?
Tożsamość
Słowo „tożsamość” pochodzi od łacińskiego określenia idem, które oznacza „tak samo”. Pojęcie wyraża podobieństwo obiektów, a na poziomie procesu zdolność rozróżniania (i grupowania) bytów o wspólnych właściwościach.
Dzięki umiejętności konstytuowania tożsamości funkcjonujemy w świecie bez ciągłego zastanawiania się nad tym, jaką relację wykształcić w stosunku do napotykanych ludzi czy przedmiotów codziennego użytku. Po prostu kojarzymy je, są nam znane. Zdolność ta jest zasługą obszarów naszego umysłu, które są w stanie w sposób przezroczysty formować tożsamości i operować na nich, tworząc modele świata, dzięki którym jesteśmy w stanie podejmować decyzje bez konieczności przetwarzania setek bodźców. Taki rodzaj reduktora.
Jak to się ma do referencji? Cierpliwości. Najpierw wyróżnimy dwa rodzaje tożsamości, ze względu na jej fundament, a potem powiążemy to z referencją.
Tożsamość może być ekstensjonalna i będzie wtedy bazowała na cechach danego obiektu (np. jego kształcie, umiejscowieniu czy przydatności do konkretnych celów). Możemy nazwać ją nieprecyzyjnie obiektywną. Taką tożsamość kojarzymy z codzienności. Przykładem może być rozpoznawanie kolegi bądź koleżanki, jako osób o pewnym wyglądzie i charakterze. Dotyczy to też rzeczy.
Istnieje również tożsamość intensjonalna, to znaczy taka, która nie zależy od właściwości identyfikowanego obiektu. Właśnie tu zastosowanie znajduje referencja. Jest sposobem budowania tożsamości, jej fundamentalnym składnikiem, gdyż pozwala na wskazywanie dowolnego bytu, tworząc tym samym abstrakt.
Abstrakty są użyteczne, bo dzięki nim możemy reprezentować skomplikowane rzeczy w postaciach na tyle uproszczonych, na ile wymaga tego ich użycie w konkretnym celu bądź zrozumienie w pewnym kontekście. Aparat fotograficzny wykonuje zdjęcie wieżowca, który jest setki razy większy. Odbitka jest abstrakcyjną wersją budynku i wystarcza, żeby ocenić jego wygląd. Bez abstraktów nie byłoby modelowania światowych procesów w komputerach, a więc i rozwiązywania problemów.
Na różnicę między intensjonalną i ekstensjonalną tożsamością możemy również spojrzeć przez pryzmat operacji. W przypadku tożsamości ekstensjonalnej (zwanej tu też funkcyjną rozszerzalnością) istotny będzie wynik operacji. W przypadku tożsamości intensjonalnej liczył się będzie sposób przeprowadzania operacji, niezależnie od otrzymywanych wartości.
Rozróżniając tożsamości intensjonalne będziemy koncentrowali się na ich przeznaczeniu
(roli) w obrębie systemu, natomiast w tożsamościach ekstensjonalnych skupimy się na
wartościach. Na przykład identyfikator x
powiązany z liczbą, napisem bądź inną
stałą daną, potraktujemy jak tożsamość ekstensjonalną, ponieważ przy porównywaniu
z inną tożsamością użyjemy właśnie tej wartości (i jej cech). Gdyby z kolei
identyfikator x
był wskaźnikiem bądź referencją do jakiejś wartości, potraktujemy
go jak tożsamość intensjonalną, gdyż podczas porównywania nie będzie istotna jego
zawartość, lecz przeznaczanie (np. wyrażanie salda rachunku w danym momencie
realizacji programu).
Tożsamość w IT
W programowaniu tożsamością będzie taki konstrukt, dzięki któremu programista (i program) jest w stanie łatwo wyróżnić pewne dane spośród innych.
W przypadku języków typowo imperatywnych będziemy mieli tożsamość ekstensjonalną (określaną miejscem w pamięci), a próby zbudowania na tej bazie tożsamości intensjonalnych będą często obarczone ryzykiem niedokładnego kapsułkowania (osłabiony aspekt kontrolny).
W przypadku wieloparadygmatowych języków o funkcyjnym rodowodzie będziemy mogli tworzyć tożsamości intensjonalne (np. z użyciem typów referencyjnych), a z uwagi na niezależność danych od umiejscowienia konstrukty takie będą faktycznie niezależne od wartości.
W przypadku języków czysto funkcyjnych tożsamość intensjonalna może okazać się zbędna z uwagi na brak przypadkowego, współdzielonego stanu i na brak konieczności jego obsługi, chociaż będziemy mogli w pewnych warunkach budować intensjonalne tożsamości (por.
reify
irunST
z Haskella).W przypadku języków obiektowo zorientowanych tożsamość będzie intensjonalna, tworzona z użyciem specjalnej właściwości każdego obiektu. Dodatkowo mogą wkradać się tam elementy ekstensjonalne, gdy język bazuje na zmiennych przestrzeniach pamięciowych, nawet jeżeli odwołania do nich nie wymagają używania adresów czy wskaźników. Poza tym niektóre z języków wprowadzają dodatkową rodzinę tzw. obiektów wartościowych, których tożsamość zależy od wartości – w ten sposób są w stanie wytwarzać tożsamości intensjonalne jako wyjątek. Problemem programisty będzie rozpoznawanie z jakim rodzajem tożsamości ma do czynienia.
W ogólnoświatowej pajęczynie (WWW) tożsamością będzie jednolity lokalizator zasobu (ang. uniform resource locator, skr. URL). Będzie to tożsamość intensjonalna, ponieważ URL nie zależy od zasobu (np. dokumentu HTML). Musimy jednak uważać, czy nie zawieruszy się tam element ekstensjonalny: nazwa domenowa.
W przestrzeni kryptowalut tożsamością będzie publiczny adres w obrębie danej sieci. To również tożsamość intensjonalna, ponieważ identyfikator nie zależy od powiązanych z nim łańcuchów transakcyjnych (odpowiednika stanu rachunku z konwencjonalnej bankowości).
W Internecie bardzo często używaną tożsamością jest nazwa systemu nazw domenowych (DNS), która identyfikuje stacje sieciowe.
Przykłady referencji w programowaniu
Referencja w programowaniu to mechanizm odwoływania się do danych z użyciem wskazującego je, pośredniego konstruktu, który przechowuje odpowiednie odniesienie. Zazwyczaj będziemy mogli je zmieniać.
Spójrzmy na wskaźnik z języka C:
Referencję z C++ (wydanie 11):
Zmienną globalną na bazie typu referencyjnego Var
z Clojure:
Zapomnijmy na chwilę, że tożsamość może być dwojaka (z powodu zakorzenienia języka programowania w imperatywnych pomysłach) i skupmy się na zademonstrowanej zdolności do pośredniego wskazywania wartości.
Każdy z przykładów zawiera element (wskaźnik, obiekt referencyjny, powiązanie) wytwarzający intensjonalną tożsamość. Co możemy o niej powiedzieć?
- Nie zależy od wartości, do której się odnosi.
- Decydujemy o tym, jakie ma znaczenie w programie.
- Może w różnych kwantach czasu wskazywać różne wartości.
Referencja w programach nie jest wartością, ale abstraktem, który odnosi się do wartości. W ten sposób pomaga nam wytwarzać intensjonalną tożsamość.
Dlaczego referencje są dobre?
Przypomnę cechy dobrej, intensjonalnej tożsamości:
- abstrakcyjność (wskazywanie dowolnych wartości),
- dynamiczność (wskazywanie różnych wartości na przestrzeni czasu),
- określoność (możliwość identyfikacji w obrębie systemu),
- unikatowość (niepowtarzalność w obrębie systemu).
Dodałbym jeszcze jedną, bardzo ważną:
zdolność kontroli.
W programowaniu możemy do niej zaliczyć również bardziej subtelne cechy tożsamości, w szczególności możliwość hermetyzowania (kapsułkowania) wartości, lecz jest to temat na odrębne opracowanie, którego podjąłem się w jednym z odcinków podręcznika do programowania w Clojure.
Poza tym w niektórych językach programowania kontrolny aspekt tożsamości będzie tak powszechny i/lub przezroczysty, że możemy nie doceniać tej cechy, chociaż w innych dziedzinach jest to właściwość, na której będzie najbardziej nam zależeć.
Gdy operujemy bezpośrednio na wartościach, na oryginalnych zasobach, wtedy zmiana ich (np. w celu dostosowania do zmieniających się warunków bądź wymagań) może być trudna i długotrwała (modyfikacje w wielu miejscach), a czasem niemożliwa do przeprowadzenia (oryginalne zasoby poza naszą kontrolą).
Przykłady z życia
Referencje to nie tylko programowanie. Są pomocne wszędzie tam, gdzie musimy coś identyfikować, aby inni byli w stanie z tego skorzystać. Warto wtedy sprawdzić kto kontroluje referencję, a jeżeli nie jesteśmy to my, spróbować to zmienić.
Ludzie dzielą się na tych,
którzy korzystają z referencji
i tych, którzy będą z nich korzystać.
Weźmy za przykład film umieszczony w popularnym serwisie wideo. Gdy w tego typu witrynie umieszczamy nasz autorski materiał, dostajemy iluzję kontroli. Dostawca usług może z różnych względów zaprzestać świadczenia usługi dla tego konkretnego utworu lub wszystkich utworów. Możemy mieć też ograniczone możliwości zmiany, np. w celu wprowadzenia drobnej poprawki. Jeżeli w danym serwisie nie da się zastąpić materiału widocznego pod pierwotnym adresem URL, nie możemy zarządzać zmianami. Ryzykowne będzie wtedy dzielenie się odnośnikami z innymi ludźmi, ponieważ wskazania do zasobów (URL-e) mogą z dnia na dzień przestać identyfikować nasze dzieła.
Gdybyśmy jednak stworzyli prosty serwis identyfikowany własną nazwą domenową, który zawierałby odnośniki do filmów (albo w dokumentach prezentowanych na stronie, albo z użyciem przekierowań), wtedy mielibyśmy całkiem niezłą, bazującą na referencjach, intensjonalną tożsamość dla każdej naszej publikacji. Prawdziwie uniwersalny identyfikator. Kiedy z „magazynem” naszych dzieł stałoby się coś niedobrego, moglibyśmy szybko przełączyć się na inny, zmieniając wartości bieżące istniejących referencji (asocjacje identyfikatorów URL). Użytkownicy nadal odwiedzaliby nasz serwis, bo to odnośniki do niego podawalibyśmy zainteresowanym. Jest to wykorzystywane na przykład w kanałach RSS z podcastami, które mimo, że publikowane w różnych serwisach, odwołują się do jednego źródłowego kanału.
Inny, powszechny przykład, to tożsamość ekstensjonalna w domenie zarządzania danymi uwierzytelniającymi i danymi kontaktowymi. To już poważniejsza sprawa.
Bardzo łatwo jest, mając konto użytkownika w popularnym serwisie internetowym, użyć go (jeżeli jest to obsługiwane) w celu logowania się do innych usług. Problem polega jednak na tym, że nie mamy kontroli nad tym zasobem i w razie kłopotów (np. blokady konta lub problemów technicznych usługodawcy) możemy zostać odcięci od innych serwisów. Dotyczy to najczęściej:
- protokołów delegowanej autoryzacji (tj. OAuth),
- skrzynek poczty elektronicznej.
Utrata dostępu do e-maila oznacza utratę naszej cyfrowej tożsamości i konieczność wykonywania żmudnych czynności aktualizacji we wszystkich miejscach, które polegają na e-mailowym adresie. Niektóre serwisy nie działają tu zbyt roztropnie i do zmiany potrzebne jest potwierdzenie wysyłane na adres bieżący, który już nie działa. Jesteśmy wtedy w kropce.
Czy oznacza to, że powinniśmy samodzielnie utrzymywać serwer poczty elektronicznej? Niekoniecznie, wystarczy nam właśnie referencja. W tym przypadku będzie nią adres e-mailowy. On sam również zawiera referencję, ponieważ składa się nie tylko z części lokalnej, ale też z nazwy domenowej. W praktyce wystarczy więc zarejestrować własną domenę u dostawcy o ugruntowanej pozycji i uruchomić przekierowanie poczty elektronicznej. Sprawi ono, że e-maile wysyłane na pozostający pod naszą kontrolą adres będą przesyłane dalej, do naszej aktualnej skrzynki, obsługiwanej u jednego z popularnych operatorów. W razie kłopotów możemy zmienić adres docelowy przekierowania bez zmiany elektronicznej tożsamości w postaci adresu powiązanego z naszą własną domeną.
W przypadku powyższego warto poświęcić chwilę i zbadać, w jaki sposób dostawca usługi poczty elektronicznej chroni się przed niechcianymi wiadomościami i dostroić tworzony mechanizm, np. wykorzystując rekordy SPF.
Referencje potrafią usprawnić zarządzanie zmianami i obniżyć ryzyko w wielu dziedzinach – szczególnie tam, gdzie mamy do czynienia z modelowaniem konkretnych, materialnych problemów. Polegając wyłącznie na wartościach będziemy wiązali nasze procesy z kształtem danych, dostępem do nich i ich charakterem. Wprowadzając referencje i przenosząc identyfikację na abstrakcyjne elementy pośrednie, jesteśmy w stanie odzyskać kontrolę i uniezależnić budowę systemu interpretacyjnego od tego, co ma być interpretowane.
Pytanie z e-maila
Pytanie, które zainspirowało mnie do podzielenia się tymi przemyśleniami było bardzo
konkretne. Dotyczyło powodu, dla którego w pliku Makefile
biblioteki
phone-number
umieszczam wywołania małych skryptów z podkatalogu
bin
aplikacji, zamiast wprowadzać tam od razu wywołania kompilatora języka Clojure
z odpowiednimi opcjami.
Oto przykład takiego skryptu, którego rolą jest uruchamianie procesu budowania dokumentacji:
A poniżej linie z pliku Makefile
, które korzystają ze skryptu docs
(zamiast
wywoływać clojure
z odpowiednimi opcjami):
W tym przypadku skrypty powłokowe pełnią funkcję referencyjną, są punktem wejściowym
dla wywołań kompilatora. Gdy będzie trzeba coś zmienić (np. zmodyfikować opcje
wywołania dla clojure
czy clj
), zrobię to właśnie tam. Nie będzie trzeba
modyfikować konfiguracji dla wszystkich innych narzędzi.
Referencje są dobre. Nie chodzi nawet o sam mechanizm, ale podejście do rozwiązywania problemów, w którym zostawiamy sobie miejsce na ujednolicony punkt kontroli dostępu do danych i wszędzie tam, gdzie się da, korzystamy z tego punktu.