Elementarz II: Podstawianie

W poprzedniej części, zapoznaliśmy się ze środowiskiem, w którym będziemy pracować. Doszliśmy do prostego edytora grafiki, który pozwala rysować myszką na ekranie, ale też podaje listę współrzędnych punktów, których połączenie prostymi liniami tworzy obrazek. Obrazek może być modyfikowany poprzez modyfikację tej listy. Ta lista jest zapisana w ściśle określony sposób, odstępstwo od składni powoduje błąd i program odmawia zinterpretowania takiego kodu. Chciałbym, żebyśmy się teraz bardziej przyjrzeli temu, z czym właściwie mamy do czynienia.

W tym odcinku:

  1. Zapis, reprezentacja
  2. Problem: złożoność
  3. Podsumowując

Zapis, reprezentacja

Narysowaliśmy obrazek, aby go utworzyć potrzebowaliśmy określonych informacji, które są niezbędne by go odtworzyć. Informacja jest czymś abstrakcyjnym, co może być reprezentowane (w tym przypadku przez obraz wyświetlony na ekranie), w tym zapisane w ścisły sposób pozwalający na wierne jego odtworzenie i modyfikację: w tym przypadku w postaci kodu o określonej strukturze.

Przyjrzyjmy się jak wygląda kod reprezentujący prostą linię:

[[[100,100],[300,300]]]

Rozłóżmy go na czynniki pierwsze. Po pierwsze, mamy nawiasy kwadratowe reprezentujące listy (ang. list) różnych elementów. Jest to podstawowy sposób wyrażania mnogości czegokolwiek, który zapewnia zachowanie kolejności, w odróżnieniu od zbioru (ang. set). Elementy listy są oddzielane przecinkami.

Widzimy od razu, że mamy listę list zawierających listy liczb. Taką strukturę nazywamy drzewem, dobrze to widać gdy przedstawimy ją w postaci diagramu:

Taką samą strukturę można zapisać na wiele sposobów:

(((100 100) (300 300)))

albo:

<obraz>
  <linia>
    <punkt x=100 y=100/>
    <punkt x=300 y=300/>
  </linia>
</obraz>

lub też:

[[{x:100, y:100}, {x:300, y:300}]]

Są to sposoby (jedne z wielu) odpowiadające istniejącym językom: Lisp, XML, JavaScript. Pierwszy format to również JavaScript, który został użyty głównie ze względu na to, że przeglądarki internetowe z łatwością sobie z nim radzą. Jak widać zapisy znacznie różnią się czytelnością i zwięzłością. Żaden z nich nie jest lepszy. Wybieramy ten, który w danym przypadku działa najlepiej.

Mamy zatem informację — byt abstrakcyjny, który nie posiada określonej formy i możemy go wyrazić na wiele sposobów (reprezentacje), w zależności od potrzeb wybieramy jeden z nich. Narysowany obrazek jest tym, co w tym przypadku chcemy przekazać, ale jest tylko wynikiem działania ekranu lub drukarki. Aby obraz pojawił się na ekranie, musi być zapisany w określonej formie w pamięci. W zależności od potrzeb stosujemy jeden z wielu możliwych zapisów. Podobnie, jeśli modyfikujemy obraz, również pracujemy na jego zapisie. Natomiast diagram dobrze nadaje się do zilustrowania struktury zapisu.

W programowaniu przeważnie pracujemy z różnego rodzaju informacją, którą możemy przedstawić na różne sposoby. Przemieszczamy ją z miejsca na miejsce (np. z pamięci operacyjnej do pamięci karty graficznej), przekształcamy w różny sposób (zapisaliśmy punkty w których zaczynają się i kończą odcinki, karta graficzna wymaga listy kolorów dla każdego kolejnego piksela). Ten fakt może nam często umknąć ze względu na abstrakcję, która sugeruje nam, że operujemy na dźwiękach, obrazach, programach, lecz wszystko się sprowadza do przetwarzania informacji.

Problem: złożoność

Powyższy przykład był prosty, lecz w realnym przypadku mamy do czynienia z większą złożonością. Moja wesoła twórczość z poprzedniego odcinka, całkiem prosty obrazek składa się z 746 punktów tworzących 38 linii. To dużo za dużo, żeby ogarnąć taki ogrom, potrzebujemy narzędzi aby sobie z nimi poradzić.

Jak już wspomniałem, aby radzić sobie ze złożonością stosujemy różnego rodzaju abstrakcje. W jaki zatem sposób moglibyśmy sobie ułatwić pracę z takim kodem? Pomyśl.

Najprostsze rozwiązanie jakie się nasuwa na myśl to wyodrębnienie i nazwanie poszczególnych elementów. Wówczas możemy dalej posługiwać się tymi zrozumiałymi dla nas określeniami. Taki kod może w pewnym stopniu sam opisywać swoją treść, co później będzie pomocne w wybraniu jego części, które chcemy zmienić.

Przykład

Błąd: brak canvas!
OK

Najpierw spróbuj samemu wyjaśnić kawałek kodu powyżej. Spróbuj przewidzieć co się stanie gdy zmienisz współrzędne przypisane do nazwy centrum. Następnie sprawdź. Spróbuj poeksperymentować z nowymi możliwościami, czy możesz w ten sposób wstawić całą linię? Czy widzisz zasadniczą różnicę w przydatności podstawienia jednego punktu i całej linii? Czy widzisz jeszcze jakieś ograniczenia?

Zmienne

Jak widzimy, zapis zmienił się nieco w stosunku do poprzednika. Dalej mamy listy współrzędnych, ale tym razem są one poprzypisywane do nazw przy pomocy znaku równości. Taki związek nazywamy zmienną (ang. variable), na znak tego, że wyrażenie ma sens dla różnych wartości. W dowolnym miejscu możemy zamiast konkretnej wartości wpisać nazwę zmiennej, wówczas w to miejsce zostanie wstawiona jej wartość.

W związku z tym, w powyższym przykładzie, zmieniając wartość zmiennej centrum, zmieniamy automatycznie kilka punktów na raz i wszystkie linie, choć zaczynające się w tych samych miejsc, spotykają się w innym punkcie. W ten sposób możemy eliminować powtórzenia, dzięki czemu możemy nie tylko ograniczyć objętość kodu, ale też ułatwiać wprowadzanie zmian, gdy chcemy zmodyfikować powtarzającą się wartość.

Jednocześnie nazwy w kodzie pełnią funkcję dokumentacyjną, stanowią podpowiedź dla czytającego kod, co on właściwie znaczy. Na przykład w ten sposób:

oko_lewe = [[300,100],[300,150]]

oko_prawe = [[325,100],[325,150]]

uśmiech = [[255,187],[260,207],[275,216],
           [285,220],[292,224],[297,226],
           [298,226],[300,226],[318,227],
           [326,228],[333,228],[349,227],
           [351,227],[360,218],[364,207],
           [366,201],[366,200],[367,191],
           [371,181],[373,179],[373,179]]

obraz = [oko_lewe, oko_prawe, uśmiech]

Ten prosty sposób — wyszczególnienie rzeczy, które są tym samym — leży nie tylko u podstaw programowania, ale nauk ścisłych w ogóle. Jego znaczenie nie sposób przecenić. Niech jednak nie zwodzi jego prostota. Wbrew pozorom często można łatwo przeoczyć wystąpienie powtórzeń i nawet gdy występują obok siebie ich związek nie jest oczywisty, bo pojawia się dopiero na wyższym poziomie abstrakcji.

Podobną zależność możemy znaleźć w językach naturalnych. Rozważmy dwa słowa: polski przedmiot i angielskie object. Patrząc powierzchownie, poza tym, że mają podobne znaczenie, te słowa nie mają wiele wspólnego. Jeśli jednak wiemy, że object pochodzi od łacińskiego objectum, a to z kolei składa się z dwóch członów: ob- (w drodze do) i jecere (rzucać = miotać), zobaczymy, że polskie słowo przedmiot jest zasadniczo kalką z łaciny (przed-miot).

Mówiąc językiem informatyki, musiałem wyabstrahować brzmienie tych słów i operować na morfemach (najmniejsze grupy dźwięków niosących określone znaczenie) aby ujrzeć podobieństwo. Swoją drogą, ten przykład wyjaśnia co ma wspólnego projekt z projektorem (rzutnikiem).

Język formalny

Zapis, którym teraz się posługujemy już prawie jest językiem programowania (stanie się nim już niebawem). W tym miejscu jednak chcę zaznaczyć kilka kwestii. Jak już wspomniałem, języki formalne (w tym języki programowania), tak samo jak języki naturalne, są kwestią umowy, może on wyglądać praktycznie dowolnie (dla ciekawych, proponuję poczytać o językach ezoterycznych jak brainfuck, whitespace czy Shakespeare), choć kwestie praktyczne sprawiają, że większość tych języków wygląda stosunkowo podobnie.

To co różni języki formalne od naturalnych to to, że muszą mieć ścisłe reguły działania, na podstawie których można jednoznacznie określić jakie jest znaczenie poszczególnych wyrażeń — w językach naturalnych zwykle zdania można zrozumieć na wiele sposobów, co nadrabiamy znajomością kontekstu w jakim są wypowiadane/pisane.

Każdy język ma swoją składnię, która określa jak poszczególne symbole można łączyć ze sobą. I tak w języku, którego używamy w tej lekcji istnieje tylko jeden rodzaj zdań (ang. statements) — przypisanie wartości zmiennej. Składa się ono z nazwy (która może się składać z dowolnych znaków oprócz cyfr i odstępów), znaku równości i wyrażenia, które oznacza. Wyrażenie zaś może być liczbą, nazwą zmiennej lub listą wyrażeń.

Jedna nazwa ma szczególne znaczenie: obraz, zawiera ona definicję całości. Brak zmiennej o tej nazwie skutkuje błędem. Błąd wystąpi również w momencie, gdy procedura podstawiania zacznie się zapętlać, np. w wyrażeniu:

a = [[20, 20], b]
b = [30, a]

obraz = [b]

Określenie wartości zmiennej b, wymaga określenia wartości zmiennej a, a ta znów odsyła nas do zmiennej b. W związku z tym nie możemy określić wartości b. Swoją drogą, nie jest to jedyny błąd. Spójrzmy na to jak to wyrażenie byłoby przekształcane:

[b] = [[30, a]]
    = [[30, [[20, b]]]]
    = [[30, [[20, [30, a]]]]]
    = ...

Nawet nie kończąc rozwijania tego wyrażenia, widzimy, że nie otrzymujemy drzewa o oczekiwanej strukturze, która była opisana wcześniej.

Błędy składniowe są raczej nieodłączną częścią programowania i są zwykle bardzo dużą przeszkodą dla początkujących programistów, którzy nie rozumieją jaka jest składnia języka, w którym piszą. Jest to kolejny powód dla którego układam ten kurs w taki sposób, abyście mogli powoli przyswajać kolejne reguły i, dzięki temu, wiedzieć co należy zrobić w danej sytuacji.

Podsumowując

W tym odcinku rozszerzyliśmy nasz język i umiejętność programowania o najbardziej podstawowe narzędzie — podstawianie. Dzięki temu możemy nazywać poszczególne elementy obrazu. To z kolei pozwala nam ograniczyć ilość kodu, czyni kod bardziej czytelnym i łatwiejszym w zarządzaniu. To jednak wciąż trochę za mało, żeby programować. Dlatego w kolejnej części zajmiemy się opisywaniem podobieństwa.

W następnej części, zajmiemy się funkcjami.