12 października 2020

Podstawy Programowania Funkcyjnego Epizod 3


Czy wszystkie Zasady Się Zmieniają?

Kiedy tylko zaczynamy używać nowego paradygmatu, porównujemy z nim nasze dotychczasowe zasady i nawyki. Pytamy siebie czy te wszystkie zasady i nawyki są poprawne w kontekście tego nowego paradygmatu. Rozważ, dla przykładu: Test Driven Development. Czy nadal jest poprawne w Programowaniu Funkcyjnym? Jeżeli tak, to jak się do tego zabierzesz?

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 07 stycznia 2013 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Aby odpowiedzieć sobie na to pytanie spróbujmy napisać prosty funkcyjny program: Word Wrap (zawijanie tekstu). Wymagania są proste. Mając napis złożony ze słów, oddzielonych pojedynczymi spacjami, i mając daną, pożądaną długość wiersza, wstaw znaki końca linii w odpowiednie miejsca w napisie, aby upewnić się, że żadna linia nie jest dłuższa niż zadana długość. Słowa mogą być ucięte tylko, jeżeli są dłuższe niż zadana długość.
Weźmy tekst Przemowy gettysburskiej jako przykład napisu:
“Four score and seven years ago our fathers brought forth upon this continent a new nation conceived in liberty and dedicated to the proposition that all men are created equal”
I gdy zawiniemy to tak, aby żadna linia nie była dłuższa niż 10 znaków, rezultat będzie taki:
“Four score\nand seven\nyears ago\nour\nfathers\nbrought\nforth upon\nthis\ncontinent\na new\nnation\nconceived\nin liberty\nand\ndedicated\nto the\npropositio\nn that all\nmen are\ncreated\nequal”
Użyjmy świetnego frameworku do testowania o nazwie Midje autorstwa Briana Maricka, i zacznijmy, tworząc pierwszy test.
(facts
  (wrap "" 1) => "")
Co oznacza, że istnieje przypadek, który wywołując (wrap "" 1) zwróci "". Aby to przechodziło definiujemy funkcję wrap i dajemy jej, jak zwykle, najprostszą implementację.
(defn wrap [s n]
  "")
Następny test prowadzi nas znowu w okolice najprostszej implementacji:
(wrap "x" 1) => "x"

(defn wrap [s n]
  s)
Następny test zmusza nas aby rozbić słowa, które są za długie
(wrap "xx" 1) => "x\nx"

(defn wrap [s n]
  (if (<= (length s) n)
    s
    (str (subs s 0 n) "\n" (subs s n))))
Instrukcja if działa w Clojure tak, jak operator warunkowy ?: w C czy w Javie. Zwraca pierwsze wyrażenie, jeżeli wyrażenie logiczne jest prawdziwe; w przeciwnym wypadku zwraca drugie wyrażenie. Funkcja subs prawie samoopisuje się i jest podobna do tej metody subString z Javy. Z kolei funkcja str po prostu łączy wszystkie swoje argumenty w jeden napis.
Następny test zmusza nas do powtarzania tego samego. Użyjemy do tego rekurencji:
(wrap "xxx" 1) => "x\nx\nx"

(defn wrap [s n]
  (if (<= (count s) n)
    s
    (str (subs s 0 n) "\n" (wrap (subs s n) n))))
Następny test sprawdza przypadek, w którym znak tuż po zawinięciu linii jest spacją. Nie chcemy mieć spacji na początku następnej linii, więc pomijamy tę spację.
(wrap "x x" 1) => "x\nx"

(defn wrap [s n]
  (if (<= (count s) n)
    s
    (let [trailing-space (= \space (get s n))
          new-line-start (if trailing-space (inc n) n)
          head (subs s 0 n)
          tail (subs s new-line-start)]
      (str head "\n" (wrap tail n)))))
Polecenie let w Clojure po prostu pozwala stworzyć lokalne symbole, które trzymają wartości użyte w ciele funkcji. To nie są zmienne, ponieważ ich wartość się nie zmienia.
Funkcja get zwraca znak na pozycji n w ciągu znaków. Wyrażenie \space jest stałą w Clojure, oznaczającą spację.
Następny test zmusza nas do szukania w tył ostatniej spacji przed tym, jak będziemy musieli złamać linię. Jeżli nie została znaleziona spacja, to oznacza, że mamy do czynienia z wyrazem dłuższym niż n. W przeciwnym wypadku dzielimy linię na tej spacji. To szukanie wstecz jest robione przy użyciu funkcji .lastIndexOf, którą wołamy bezpośrednio z biblioteki stringów Javy. To właśnie kropka . wskazuje na bezpośrednie odwołanie do Javy.
(wrap "x x" 2) => "x\nx"

(defn wrap [s n]
  (if (<= (count s) n)
    s
    (let [space-before-end (.lastIndexOf s " " n)
          this-line-end (if (neg? space-before-end)
                            n
                            space-before-end)
          trailing-space (= \space (get s this-line-end))
          new-line-start (if trailing-space
                             (inc this-line-end)
                             this-line-end)
          head (subs s 0 this-line-end)
          tail (subs s new-line-start)]
      (str head "\n" (wrap tail n)))))
Zrobiło się to trochę nieładne, więc zrefaktorujmy to troszkę
(defn find-start-and-end [s n]
  (let [space-before-end (.lastIndexOf s " " n)
        line-end (if (neg? space-before-end) n space-before-end)
        trailing-space (= \space (get s line-end))
        line-start (if trailing-space (inc line-end) line-end)]
    [line-start line-end]))

(defn wrap [s n]
  (if (<= (count s) n)
    s
    (let [[start end] (find-start-and-end s n)
          head (subs s 0 end)
          tail (subs s start)]
      (str head "\n" (wrap tail n)))))
Możesz być nieprzekonany, że to wszystkie przypadki testowe. Więc zachęcam Cię do poszukania nowych. Zachęcam Cię do przestudiowania tych sześciu prostych przypadków testowych powoli i dokładnie. Większość ludzi nie pisze ich w ten sposób. Dużo czasu mi zajęło, zanim przekonałem się o wartości dobrze poukładanych, ostrożnie przemyślanych, minimalnych do bólu przypadków testowych.
Pomimo tego, wydaje się, przynajmniej w tym przypadku, że zasady TDD nadają się do wykorzystania tak samo w programowaniu funkcyjnym, jak i innych typach programowania. Zatem, gdy zaczynasz z programowaniem funcyjnym nie wylewaj dziecka z kąpielą. Trzymaj się swojej dyscypliny TDD!
Niektórzy mogliby zarzucić, ten algorytm nie jest funkcyjny; ale zapewniam Cię, że taki jest. Nie ma żadnych skutków ubocznych. Jest reprezentacyjnie przejrzysty. Używa trwałych struktur danych. Jest funkcyjny.
Brakuje mu rzeczywiście jednego przymiotu, który zwykle znajduje się w programach funkcyjnych. Nie jest złożony z zestawu przekształceń.
Rozważ jeszcze raz program do obliczania kwadratów liczb całkowitych.
(take 25 (squares-of (integers))
Czysta elegancja tego kawałka kodu wynika częściowo z faktu, że jest to właśnie seria przekształceń. Pierwsze przekształcenie to funkcja integers. Przekształca nic w leniwą sekwencję liczb całkowitych zaczynających się od 1. Drugie przekształcenie to funkcja squares-of. Przekształca dowolną listę liczb całkowitych w listę ich kwadratów. Ostatnie przekształcenie to funkcja take, która przekształca nieskończoną leniwą sekwencję w sekwencję o dokładnie 25 elementach.
Czy możemy przepisać program do zawijania tekstu do programu, w którym występują tylko serie przekształceń. Jasne. Rozważ to:
(defn wrap [s n]
  (join "\n"
        (make-lines-up-to n
                          (break-long-words n (split s #" ")))))
Bedziemy czytać ten kawałek kodu w sposób, w jaki czytaliśmy program kwadraty liczb całkowitych; od środka w górę. Najpierw musimy rozbić wejściowy napis w sekwencję słów. Potem rozbijamy wszystkie długie słowa dłuższe niż n na dwa lub więcej słów. Następnie dokładamy te słowa do linii które są nie dłuższe niż n. Na koniec łączymy wszystkie linie razem z "\n". Proste, prawda?
No cóż, wygląda na prosty, gdy jest napisany w ten sposób. A tutaj implementacja całego programu:
(defn break-long-words [n words]
  (if (empty? words)
    []
    (let [word (first words)]
      (if (>= n (count word))
        (cons word (break-long-words n (rest words)))
        (cons (subs word 0 n)
              (break-long-words
                 n
                 (cons (subs word n) (rest words))))))))

(defn make-lines-up-to
  ([n words]
    (make-lines-up-to n words [] []))
  ([n words line lines]
    (if (empty? words)
      (conj lines (join " " line))
      (let [new-line (conj line (first words))]
        (if (>= n (count (join " " new-line)))
          (make-lines-up-to n (rest words) new-line lines)
          (make-lines-up-to n
                            words
                            []
                            (conj lines (join " " line))))))))

(defn wrap [s n]
  (join "\n" (make-lines-up-to
                n
                (break-long-words n (split s #" ")))))
Jest na świecie wiele osób, którzy chętnie powiedzą Ci, że nie jestem najlepszym programistą funkcyjnym w okolicy. Zapewne mają rację. Z tego względu ten program może być żałośnie niewystarczający jako demonstracja. Nadal, bardzo trudno jest mi uwierzyć, że moje pierwsze podejście do rozwiązania problemu zawijania tekstu jest gorsze od kodu powyżej. To też nie oznacza, że sekwencje przekształceń są złe. Wręcz przeciwnie, uważam, że są mają w sobie ogromny potencjał i bedziemy rozważać je jeszcze później w szczegółach. Chodzi mi o to, że nie wszystkie programy funkcyjne muszą być sekwencjami przekształceń. Stara zasada "im prościej tym lepiej" nadal aktualna. No i dla algorytmu zawijania tekstu, przynajmniej tak, jak ja to widzę (przyznam niezbyt daleko) rozwiązanie przekształceniowe nie jest najlepsze.
Tak przy okazji, nie używałem TDD do stworzenia tego rozwiązania przekształceniowego, które widzisz powyżej. Pewnie mógłbym, ale zamiast tego użyłem metody, którą preferuje wielu programistów funkcyjnych. Rozważałem to dłuższą chwilę, aż w końcu użyłem ręcznego testowania w konsoli (zwykle nazywa się to REPL, Read Evaluate Print Loop czyli Pętla Wczytaj Wykonaj Wyświetl) aby poskładać wszystko w całość. Cały proces zajął mi przynajmniej trzy razy tyle niż TDD, włączając wiele okropnego debbugowania i komend wyświetlających, co spowodowało mnóstwo rekursywnym pętli nieskończonych, które rozwalały stos i wywalały mi konsolę. Czyniąc długą sprawę krótką, to był ból. Nie polecam tego.
Podsumowując: stare zasady nadal działają. Programowanie funkcyjne może być zmianą - ważną zmianą, ale nie zmienia ono wszystkiego. Programowanie to nadal programowanie. Dyscyplina to nadal dyscyplina. TDD działa w programowaniu funkcyjnym tak dobrze jak w innych stylach programowania. Być może są jakieś zasady, z których musielibyśmy zrezygnować wdrażając programowanie funkcyjne; ale póki co ja żadnej takiej nie znalazłem.

Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 07 stycznia 2013 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

7 marca 2019

Podstawy Programowania Funkcyjnego Epizod 2


Dlaczego to programowanie nazywa się funkcyjne?

W poprzednim odcinku opowiedziałem Ci, o co chodzi z tym całym zgiełkiem wokół programowania funkcyjnego. Pamiętasz? Przejrzystość Referencyjna i pociąg towarowy z wieloma rdzeniami? Skoro jesteś tu, i czytasz odcinek 2., zakładam, że moje argumenty przekonały Cię i chcesz dowiedzieć się czegoś więcej.

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 02 stycznia 2013 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


A więc następnym pytaniem, które czeka na odpowiedź, jest: Dlaczego to nazywa się "Funkcyjnym" programowaniem? Prostą odpowiedzią na to pytanie jest to, że Programowanie Funkcyjne to programowanie przy użyciu funkcji (ehhh). Spośród wszystkich odpowiedzi, ta jest całkiem słaba. No, jest akurat precyzyjna, i brzmi całkiem nieźle; ale tak naprawdę nie odpowiada na nasze pytanie. W końcu, programy napisane w Javie są programami z funkcjami.
A więc dlaczego słowo "funkcyjne"?
Wkurzę pewnie w tej chwili wszystkich matematycznych perfekcjonistów; ponieważ zamierzam użyć analogii z rachunkiem różniczkowym. Nie będę się tym przejmował za bardzo, ponieważ po prostu ukradłem ten pomysł z Wikipedii.
Czy wiesz, co znaczy $\frac{dy}{dx}$? W szczególności, w wyrażeniu $\frac{dy}{dx}$ czym jest $y$? Oczywiście $y$ jest funkcją. Wyrażenie $\frac{dy}{dx}$ jest pochodną tej funkcji. Czyli $\frac{dy}{dx}$ przyjmuje funkcję jako swój argument wejściowy i zwraca pochodną tej funkcji jako wynik. Mam rację?
Rozważ $\frac{d(x^2)}{dx}$: to równa się $2x$. Zauważ, że $2x$ jest funkcją. Więc argument wejściowy i wyjściowa wartość są obie funkcjami.
Czekaj. Jak nazwiesz coś, co przyjmuje argumenty i zwraca wartość? Nazwiesz to funkcją. A więc $\frac{dy}{dx}$ jest funkcją, która przyjmuje funkcję i zwraca funkcję.
Co by było, gdybyśmy mieli taki język programowania komputerów? Co by było, gdybyśmy mogli pisać funkcje, które przyjmują funkcje jako argumenty, operować na nich, bez rozwiązywania ich, i potem zwracać nowe funkcje jak wynik? Jak byś nazwał taki język? Jakie inne słowo mogłoby być lepsze niż: funkcyjny?
I już widzę tych wściekłych na mnie purystów języków funkcyjnych wściekłych za to, że to kompletnie nieodpowiedni sposób definicji języka funkcyjnego. Ale to jest jeden krok do przodu – i to bardzo ważny krok.
A więc, co to znaczy przesłać funkcję jako argument do innej funkcji? Spójrzmy jeszcze raz na program "kwadraty liczb całkowitych". Pamiętasz? To było po prostu:
(take 25 (squares-of (integers)))
Zróbmy z tego funkcję, dodając nazwę i definicję argumentu:
(defn squint [n]
  (take n (squares-of (integers))))
Jestem pewny, że ogarniasz składnię sam. To nic trudnego.
Mając definicję squint, możemy wyświetlić 25 kwadratów liczb całkowitych przy użyciu:
(println (squint 25))
Jak dotąd, to nic innego, jak tylko proste wywołania funkcji i definicje. Ale czym jest ta funkcja squares-of. Jak jest zdefiniowana?
(defn square [n] (* n n))

(defn squares-of [list]
  (map square list))
Teraz zaczyna się robić trochę bardziej interesująco! Nie jest niespodzianką, że funkcja square po prostu mnoży argument przez jego samego. To definicja squares-of jest interesująca, ponieważ przesyła funkcję square jako argument do funkcji o nazwie map.
Funkcja map jest jedną z podstaw programowania funkcyjnego. Pobiera dwa argumenty: funkcję f i listę l; i zwraca nową listę, wywołując funkcję f na każdym elemencie listy l.
Przesyłanie funkcji jako argument to nie jest coś, co programiści Javy robią często. Z drugiej strony nie jest to zupełnie obcy pomysł. Każdy programista Javy, który uczył się Wzorca Polecenie, lub używał Listenerów w Swingu, zrozumie, co tu się dzieje.
Co więcej, obecnie coraz więcej języków programowania zaczęło wykorzystywać pomysł funkcji przekazywanych jako argumenty. Ta funkcjonalność jest czasem nazywana "bloki" lub "lambdy". Jest bardzo częstym elementem języka Ruby; stała się ostatnio ważną częścią C#. Chodzą słuchy, że ta funkcjonalność zostanie dodana w Javie niedługo.[1]
Więc może nie będziemy musieli uczyć się nowego języka. Może nasze stare języki będą stawały się coraz bardziej i bardziej funkcyjne i będziemy mogli używać tych funkcyjnych funkcjonalności, jak tylko staną się on coraz bardziej dostępne.
Ta droga prowadzi do szaleństwa, płaczu i zgrzytania zębami! Język z elementami funkcyjnymi nie jest językiem funkcyjnym; i program napisany z kilkoma lambdami tu i tam, nie jest funkcyjnym programem.
Żeby przekonać się, dlaczego to prawda, musimy spojrzeć na funkcję integers.
(defn integers []
  (integers-starting-at 1))
OK, to nic trudnego. Funkcja integers-starting-at po prostu przyjmuje liczbę całkowitą jako argument i zwraca listę wszystkich liczb całkowitych, zaczynając od tego, co jest w argumencie.
(defn integers-starting-at [n]
  (cons n (lazy-seq (integers-starting-at (inc n)))))
Ten przykład wymaga słówka wyjaśnienia. Ale nie bój się, to jest właściwie całkiem proste.
Na początku jest funkcja cons; która jest skrótem dla "zbuduj listę" (construct list). Funkcja cons przyjmuje dwa argumenty: element, i listę. Zwraca nową listę jako starą listę z dołożonym z przodu elementem. Czyli (cons 2 [3 4]) zwraca [2 3 4].
Teraz z kolei wszyscy ludzie od Clojure są wkurzeni na mnie, bo to nie jest do końca tak. Z drugiej strony różnica jest czymś, czym nie musimy się teraz przejmować. Na tę chwilę zapamiętaj tylko to: funkcja cons tworzy listę poprzez dołożenie jej pierwszego argumentu do drugiego; który to drugi musi być listą.
Teraz możesz sobie pomyśleć, że cons po prostu przyczepia ten element z przodu listy; ale to nie jest tak. W rzeczywistości, to nie jest nawet bliskie prawdy. Widzisz, cons w żaden sposób nie zmienia listy z drugiego argumentu. Zamiast tego zwraca całkiem nową listę złożoną z zawartości listy z drugiego argumentu i elementu dodanego na początku.
O kurcze, teraz to już ludzie od Clojure są na mnie naprawdę źli; ponieważ to znowu nie do końca prawda. I pewnie masz mnie za wariata, bo co za głupek kopiowałby całą zawartość jednej listy do drugiej listy tylko po to, żeby wrzucić jeden element na początek?
OK, więc tak, cons w zasadzie rzeczywiście wkłada element na początek listy, i nie, nie kopiuje starej listy do nowej listy. Z drugiej strony, robi to w taki sposób, że możesz udawać, że listy wejściowa i wyjściowa są zupełnie inne. Praktycznie rzecz biorąc, wyjście cons jest całkowicie nową listą - nawet, jeśli tak nie jest.
Zbity z tropu? Nie martw się, w rzeczywistości to nie jest takie trudne do zrozumienia. Rozważ listę [1 2 3]. Jeżeli zaimplementujemy ją jako listę jednokierunkową, będzie wyglądała tak: 1->2->3. Teraz odpalmy cons z 0 na początku. To daje nam 0->1->2->3. Zauważ jednak, że stara lista nadal istnieje w nowej liście. To jest ta tajemnica! Stara lista pozostaje niezmieniona w nowej liście. Więc możemy stworzyć pozory, że cons zwróci całkiem nową listę, pozostawiając starą listę niezmienioną.
To jest wskazówka do wyjaśnienia tego, co wszystkie prawdziwe języki funkcyjne tak naprawdę robią. One pozwalają Ci tworzyć coś, co wydaje Ci się być całkowicie nowymi strukturami danych, podczas gdy zachowują stare struktury danych nietknięte; i robią to bez kopiowania. W kręgach funkcyjnych to jest znane jako trwałość; co więcej, struktury danych, które zachowują się w ten sposób, są znane jako trwałe struktury danych. Takie struktury danych zarządzają cały czas swoją historią. Nic nigdy nie zostaje wyedytowane lub skasowane w ich wnętrzu. Jednak mają one wiele różnych "punktów-wejścia" i każdy z nich zapewnia inny widok na dane. Pomyśl o tym, jak o systemie zarządzania kodem źródłowym. Chociaż kasujesz i edytujesz linie kodu, nic nigdy nie kasuje się ani nie edytuje w repozytorium kodu źródłowego. Cała historia jest zachowana. Po prostu masz różne punkty wejścia do kodu źródłowego, które pozwalają Ci widzieć różne migawki z przeszłości.


A więc wróćmy do naszego programu. Spójrzmy na integers-starting-at
(defn integers-starting-at [n]
  (cons n (lazy-seq (integers-starting-at (inc n)))))
Co jest tym, co funkcja cons dodaje z przodu i z przodu czego to jest dodawane? Podczas pierwszego przejścia n będzie 1, więc cons zwróci listę, która rozpoczyna się od 1. To ma oczywisty sens, ponieważ naszym celem jest wyświetlenie kwadratu z 1.
OK, ale co jest to coś, do którego cons dokłada 1? To jasne, że 1 jest dodawana na początku listy zwracanej przez funkcję lazy-seq.
Tutaj zaczyna się magia. Widzisz, lazy-seq jest funkcją, która zwraca tzw. leniwą sekwencję. Leniwa sekwencja to zwykła lista — ale z niespodzianką. Zamiast listy jednokierunkowej z wartościami takimi, jak 1->2->3, leniwa sekwencja jest wartością połączoną z funkcją: 1->f. Ta funkcja, kiedy jest wywołana, zwraca następną wartość połączoną z inną funkcją: 2->f. I z jaką funkcją te wartości są połączone? Popatrz dobrze! To argument funkcji lazy-seq.
Tym argumentem funkcji lazy-seq jest rekurencyjne wywołanie funkcji integers-starting-at z argumentem wartości funkcji (inc n). Funkcja inc po prostu zwraca jej argument powiększony o 1.
A więc czym jest leniwa sekwencja? Leniwa sekwencja jest listą, której elementy są obliczane tylko wtedy, kiedy są potrzebne, i nie wcześniej. Za każdym razem, kiedy prosisz leniwą sekwencję o następny element w liście, ona po prostu wywołuje funkcję, która jest powiązana z aktualnym elementem.
Stąd, leniwa sekwencja nie ma rozmiaru. Możesz wciąż i wciąż pytać o więcej elementów w nieskończoność; i wciąż te elementy nie zostaną obliczone, dopóki to nie będzie potrzebne.
Funkcja map też zwraca leniwą sekwencję. Tu jest przykład implementacji:
(defn map [f l]
  (if (empty? l)
    []
    (cons (f (first l)) (lazy-seq (map f (rest l))))))
Funkcja first po prostu zwraca pierwszy element listy będącej jej argumentem. Funkcja rest po prostu zwraca listę z argumentu minus jej pierwszy element; czyli resztę listy. Więc map jest po prostu rekurencyjną funkcją, która odpala funkcję f na każdym elemencie listy l, tworząc nową leniwą sekwencję wyników.
Została nam jeszcze jedna rzecz, zanim złożymy wszystko razem: funkcja take. Po tym wszystkim, co do tej pory razem przeszliśmy, ta będzie naprawdę bardzo prosta.
(defn take [n l]
  (if (zero? n)
    []
    (cons (first l) (take (dec n) (rest l)))))
Jak widzisz, funkcja take zwraca listę złożoną z pierwszych n elementów listy l.
A teraz, poćwiczmy trochę Przejrzystość Referencyjną, i rozkodujmy (ręcznie) wartość wywołania:
(squint 2)
Najpierw podmieńmy squint jej definicją.
Potem podmieńmy take jej definicją. To jest trochę zawiłe, bo musimy użyć rekurencji.
(if (zero? 2)
  []
  (cons (first (squares-of (integers)))
    (if (zero? (dec 2))
      []
      (cons (first (rest (squares-of (integers))))
        (if (zero? (dec (dec 2)))
          []
          ...)))))
Zatrzymałem się na trzecim wyrażeniu if, ponieważ (dec (dec 2)) wynosi zero. W rzeczywistości znamy wartość logiczną tych if-ów, więc możemy usunąć je wszystkie:
(cons (first (squares-of (integers)))
  (cons (first (rest (squares-of (integers))))
    []))
To jasne, że mając tylko dwa wywołania funkcji cons, ten fragment kodu zwróci listę z dwoma elementami w środku. Możemy trochę posprzątać, zamieniając wywołania funkcji integers z ich definicjami.
(cons (first (squares-of (integers-starting-at 1)))
 (cons (first (rest (squares-of (integers-starting-at 1))))
   []))
Ponieważ wywołanie (squares-of (integers-starting-at 1)) występuje dwa razy, obliczmy je raz i zamieńmy wywołania z ich wartościami. Zaczniemy zamianę od squares-of:
(map square (integers-starting-at 1)
A potem integers-starting-at:
(map square (cons 1 (lazy-seq (integers-starting-at 2)))
Teraz zamieńmy map. Ponieważ map zaczyna się z (if (empty? l) ...), i ponieważ (cons 1...) gwarantuje, że lista nie będzie pusta, możemy pominąć wyrażenie if i użyć tylko ciała funkcji.
(cons (square (first (cons 1 (lazy-seq...))))
  (map square (rest (lazy-seq (integers-starting-at 2)))))
Te wywołanie do first może być łatwo zredukowane:
(cons (square 1)
  (map square (rest (lazy-seq (integers-starting-at 2)))))
A teraz odrobinę więcej magii. Wywołanie funkcji rest zwraca "resztę" listy poprzez wywołanie funkcji przesłanej do lazy-seq
(cons (square 1)
  (map square (integers-starting-at 2)))
Możemy powtórzyć tę analizę dla (map square (integers-starting-at 2)):
(cons (square 1)
  (cons (square 2)
    (map square (integers-starting-at 3)))
I teraz możemy zredukować funkcję squares
(cons 1
  (cons 4
    (map square (integers-starting-at 3)))
Zostawiliśmy naszą poprzednią analizę dla całej funkcji w następującym stanie:
(cons (first (squares-of (integers-starting-at 1)))
  (cons (first (rest (squares-of (integers-starting-at 1))))
    []))
Teraz możemy wstrzyknąć naszą wartość dla (squares-of (integers-starting-at 1)).
(cons
  (first (cons 1
            (cons 4
            (map square (integers-starting-at 3))))
  (cons
    (first (rest (cons 1
                    (cons 4
                    (map square (integers-starting-at 3)))))
    []))
Pierwsze first jest łatwo zredukować. (first (cons x 1)) to po prostu x; więc możemy zignorować drugi argument funkcji cons.
(cons
  1
  (cons
    (first (rest (cons 1
                       (cons 4
                       (map square (integers-starting-at 3)))))
    []))
Te (first (rest...)) jest także łatwo obliczyć.
(cons
  1
  (cons 4 []))
I wynik tego, to oczywiście [1 4].
Zauważyłeś, co stało się z tym (integers-starting-at 3)? Nigdy nie zostało obliczone. Dlaczego? Ponieważ oryginalna (take 2...) potrzebowała tylko 2 wartości, więc trzecia nigdy nie była potrzebna.
I to prowadzi do ważnego odkrycia. Większość z nas pisze programy, które przepychają dane przez całość programu. Ale (take 25 (squares-of (integers))) jest pętlą, która ciągnie dane poprzez całość programu. To jest gruntowna różnica, i coś, nad czym później zamierzamy spędzić dużo więcej czasu.
Do tego momentu wszyscy ludzie od Clojure, wszyscy ludzie od programowania funkcyjnego, i wszyscy ludzie od matematyki są już na mnie porządnie wściekli; ponieważ tak bardzo uprościłem. I to jest prawda; jest ogrom rzeczy, po których tylko się prześlizgnąłem i na które machnąłem ręką. I nadal to, co Ci tu przekazałem, jest w rzeczywistości poprawne. Jest to także całkiem niezła prezentacja potęgi traktowania funkcji jako danych, które mogą być przesyłane i zwracane z funkcji.
I to prowadzi nas do sedna tego epizodu. Teraz już możemy odpowiedzieć na pytanie postawione pod tytułem. Nazywamy ten styl programowania funkcyjnym, ponieważ to wszystko jest o traktowaniu funkcji jak cząstek danych, którymi manipuluje się w taki sam sposób jak znakami czy liczbami. Ludzie od programowania funkcyjnego odnoszą się do nich jako do typów pierwszoklasowych. Język funkcyjny jest językiem, który używa funkcji jako typów pierwszoklasowych, promuje przejrzystość referencyjną poprzez usuwanie przypisań, i zachowuje historię danych poprzez trwałe struktury danych.
Jak nauczymy się w następnych odcinkach, ta definicja nie jest ani pełna, ani w pełni właściwa; ale na tę chwilę... wystarczy.

Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 02 stycznia 2013 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

[1] - Została dodana dopiero 18 marca 2014 roku wraz z 8. wersją Javy. [przyp. tłum.]

18 lutego 2019

Podstawy Programowania Funkcyjnego Epizod 1


O czym jest programowanie funkcyjne?

Zakładam, że słyszałeś już kiedyś o programowaniu funkcyjnym. No cóż, któż nie słyszał? Wszyscy o tym gadają. Wychodzi dużo nowych języków funkcyjnych takich, jak Scala, F# i Clojure. Ludzie rozmawiają też o starszych językach jak Erlang, Haskell, ML i innych.
A więc, o co w tym wszystkim chodzi? Dlaczego programowanie funkcyjne jest Następną Wielką Rzeczą™? I co jest w tym takiego pociągającego?

Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 22 grudnia 2012 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Po pierwsze, prawie na pewno programowanie funkcyjne jest następną wielką rzeczą. Są ku temu dobre, solidne powody i poznamy je w tym artykule. Ale najpierw, aby zrozumieć te powody, musimy poznać, czym programowanie funkcyjne jest. Wkurzę wielu ludzi moim kolejnym stwierdzeniem, ponieważ zamierzam uciec się do skrajnego minimalizmu. Ograniczę programowanie funkcyjne do jego najgłębszej istoty; i to nie jest do końca w porządku, bo temat jest bogaty, obszerny i pełen fantastycznych pomysłów. Ogarniemy te pomysły w przyszłych artykułach. Zobaczysz niektóre z nich już tutaj. Ale na razie, zdefiniuję programowanie funkcyjne w taki sposób:
Programowanie funkcyjne jest programowaniem bez instrukcji przypisania.
O nie! A jednak to zrobiłem. Teraz programiści funkcyjni zbierają się przeciwko mnie, trzymając w rękach widły i pochodnie. Chcą mojej głowy za wypowiedzenie tej minimalistycznej blasfemii. Tymczasem wszyscy goście, którzy mieli nadzieję nauczyć się czegoś o programowaniu funkcyjnym, naprawdę przestali czytać z powodu powyższego zdania, które jest tak rażąco głupie. To znaczy: jak w ogóle można programować bez instrukcji przypisania? Najlepszym sposobem pokazania tego jest przykład. Spójrzmy na bardzo prosty program w Javie: Kwadraty liczb całkowitych.

public class Squint {
  public  static  void  main(String  args[]) {
    for  (int  i=1;  i<=25;  i++)
      System.out.println(i*i)
  }
}



Któż z nas nie napisał tego lub czegoś bardzo podobnego? Ja chyba pisałem to setki razy. To jest zwykle drugi program, który piszę w języku, którego się uczę i trzeci czy czwarty w kolejności, gdy uczę młodych programistów pisać programy. Każdy zna stare, dobre kwadraty liczb całkowitych!
Ale przyjrzyjmy się temu uważnie. To jest tylko zwykła pętla ze zmienną nazwaną i, która liczy od 1 do 25. Każde przejście pętli sprawia, że zmienna i przybiera nową wartość. To jest przypisanie. Nowa wartość jest przypisywana do zmiennej i podczas każdego przejścia pętli. Gdybyś mógł jakoś podejrzeć pamięć komputera i dojrzeć miejsce w pamięci, które przechowuje wartość zmiennej i, zauważyłbyś, że ta wartość zmienia się podczas każdej iteracji przejścia pętli.
Jeżeli poprzedni paragraf wydawał się rozwodzić się nad oczywistością, to tylko powiem, że całe artykuły naukowe napisano na ten temat. Pojęcia równoważności, wartości i stanu mogą wydawać się nam oczywiste; ale w rzeczywistości są one same w sobie zagadnieniami bardzo obszernymi. Ale zbaczam za bardzo.
Teraz spójrzmy na funkcyjny odpowiednik programu kwadratów liczb całkowitych. Użyjemy do tego Clojure; choć pomysły, które przerobimy, działają tak samo w każdym innym języku funkcyjnym.

(take 25 (squares-of (integers)))
Tak, dobrze czytasz; powiem więcej: to jest program, który wyświetla prawidłowe wartości. Jeśli chcesz zobaczyć wyniki, oto one:

(1 4 9 16 25 36 49 64 ... 576 625)
W tym programie użyto trzech słów. Każde z tych słów odnosi się do funkcji. Nawiasy z lewej strony tych wyrazów znaczą po prostu: zawołaj tę funkcję i potraktuj wszystko po prawej stronie, aż do prawego nawiasu, jako jej parametry.
Funkcja take przyjmuje dwa argumenty, liczbę całkowitą n i listę l. Zwraca pierwsze n elementów listy l. Funkcja squares-of pobiera listę liczb całkowitych i zwraca listę ich kwadratów. Funkcja integers zwraca listę kolejnych liczb całkowitych, zaczynając od 1. To wszystko. Program po prostu pobiera pierwszych 25 elementów listy kwadratów kolejnych liczb całkowitych, zaczynając od 1.
Spójrz na tę linijkę jeszcze raz; ponieważ zrobiłem tam coś bardzo ważnego. Wziąłem trzy oddzielne definicje funkcji i połączyłem je w pojedyncze zdanie. To się nazywa: (jesteś gotowy na słowo klucz?)
[w tle: Fanfary, serpentyny, konfetti, tłumy szaleją]

Przejrzystość referencyjna znaczy po prostu, że w danym zdaniu, możesz zmieniać kolejność słów razem z ich definicjami, i nie zmienia się jednocześnie znaczenie tego zdania. Lub, co ważne dla naszych zastosowań, oznacza to, że możesz zastąpić każde wywołanie funkcji wartością, którą ta funkcja zwraca. Zobaczmy to w akcji.
Wywołanie funkcji (integers) zwraca (1 2 3 4 5 6 ...) No dobra, pewnie nasuwają Ci się od razu pytania, prawda? To znaczy, jak wielka ma to być lista? Prawdziwa odpowiedź na to pytanie jest taka, że lista ma być taka duża, jak jest potrzeba, żeby była; ale nie myślmy o tym teraz. Powrócimy do tego w następnym artykule. Na ten moment przyjmijmy, że (integers) zwraca (1 2 3 4 5 6 ...); bo zwraca!
Teraz w naszym programie możemy zastąpić wywołanie funkcji (integers) jej wartością. Program po prostu staje się:

(take 25 (squares-of (1 2 3 4 5 6 ...)))
A tak, zrobiłem to przy użyciu copy paste'a; i to też jest ważny punkt. Przejrzystość Referencyjna jest tym samym co kopiowanie wartości funkcji i wklejanie jej ponad wywołaniem tej funkcji.
Teraz zróbmy następny krok. Wywołanie funkcji: (squares-of (1 2 3 4 5 6 ...)) po prostu zwraca listę kwadratów liczb z listy jej argumentów. Więc ona zwraca: (1 4 9 16 25 36 49 64 ...). Jeżeli zamienimy wywołanie tej funkcji z jej wartością, program stanie się:

(take 25 (1 4 9 16 25 36 49 64 ...))
I oczywiście wartość wywołania tej funkcji to po prostu:

(1 4 9 16 25 36 49 64 ... 576 625)
A teraz popatrzmy na ten program jeszcze raz:

(take 25 (squares-of (integers)))
Zauważ, że nie ma zmiennych. W rzeczywistości nie ma tam nic innego, tylko trzy funkcje i jedna stała. Spróbuj napisać kwadraty liczb całkowitych w Javie, nie używając ani jednej zmiennej. Oh, jest prawdopodobnie sposób, żeby to zrobić, ale z pewnością nie jest to naturalne i nie czytałoby się tego tak przyjemnie, jak mój program wyżej.
Co ważniejsze, jeżeli mógłbyś zajrzeć do pamięci komputera i spojrzeć na miejsca w pamięci używane przez mój program, odkryłbyś, że te miejsca zostałyby zainicjowane w momencie użycia ich przez program; ale ich wartości pozostałyby niezmienne przez resztę czasu wykonania programu. Innymi słowy, żadne nowe wartości nie zostałyby przypisane do tych miejsc.
W rzeczy samej to jest konieczny warunek dla Przejrzystości Referencyjnej, który opiera się na fakcie, że za każdym razem, kiedy wywołujesz konkretną funkcję, dostajesz taki sam wynik. Fakt, że pamięć mojego komputera nie zmienia się podczas uruchomienia mojego programu, oznacza, że wywołanie funkcji (f 1) zwraca zawsze tę samą wartość, niezależnie od tego, ile razu była wywołana. A to oznacza, że mogę podmienić (f 1) jej wartością, kiedykolwiek się pojawi.
Albo mówiąc jeszcze inaczej: Przejrzystość Referencyjna oznacza, że żadna funkcja nie może mieć skutków ubocznych. I oczywiście to oznacza, że żadna zmienna, raz zainicjowana, nie może nigdy zmienić swojej wartości; wszak przypisanie jest sednem skutku ubocznego.
A więc dlaczego to jest takie ważne? Co jest takiego wspaniałego w Przejrzystości Referencyjnej? Gdy wiemy, że jest możliwe pisanie programów bez przypisań, dlaczego to takie ważne?
Prawie na pewno czytasz ten tekst na ekranie. A jeśli nie; komputer znajduje się niedaleko. Jak wiele ma rdzeni? Piszę ten artykuł na MacBooku Pro z 4 rzeczywistymi rdzeniami (Mówią, że ma 8, ale nie polegałbym bardzo na tym całym "nonsensie hyper-threading". Ma cztery). Mój poprzedni laptop miał dwa rdzenie. I ten poprzednio miał tylko jeden. Jedyny wniosek, jaki z tego mogę wysnuć to, że mój następny laptop będzie miał 8 prawdziwych rdzeni; i następny-następny mógłby mieć już nawet 16.
Biedni inżynierowie hardware'u, którzy nieśli nas na plecach przez ostatnie cztery dziesięciolecia, osiągnęli w końcu prędkość światła. Zegary komputerów po prostu nie będą poruszały się już znacząco szybciej. Po tym, jak ta prędkość podwajała się co 18 miesięcy, przez okres dłuższy niż większość programistów żyje (oprócz mnie), gwałtowny wzrost prędkości komputerów zatrzymał się, jak dotychczas nie ruszając się znowu.
A więc Ci inżynierowie sprzętu, w pogoni za zaoferowaniem nam coraz większej i większej ilości cykli na sekundę, zaczęli dodawać coraz więcej i więcej procesorów do naszych układów; i nie widać odpowiedzi na pytanie: do ilu procesorów w jednym układzie doprowadzi nas ten marsz ku przyszłości.
A więc pozwól, że zapytam Ciebie, O zdolny i kompetentny programisto: Jak zapewnisz sobie przewagę w wykorzystaniu cykli dostępnego Ci procesora, kiedy Twój komputer będzie miał 4096 rdzeni w środku? Jak zarządzisz wywołaniami swoich funkcji, jeżeli będą one wszystkie działać na 16384 procesorach na tej samej szynie pamięci? Jak zbudujesz responsywne i przygotowane na zmiany strony internetowe, kiedy Twoje modele, kontrolery i widoki będą musiały współdzielić 65536 procesorów?
Mówiąc szczerze, my programiści ledwo umiemy sprawić, by dwa wątki w Javie współpracowały ze sobą. Wątki to kaszka z mleczkiem dla niemowlaka w porównaniu ze schabowym z ziemniakami rzeczywistej rywalizacji procesorów na szynie pamięci. Przez ostatnie ponad pół wieku programistom udał się zaobserwować, że procesy odpalone na komputerze są współbieżne, a nie jednoczesne. A więc chłopcy i dziewczęta, witamy w fantastycznym świecie jednoczesności! A teraz jak sobie z tym dacie radę?
Odpowiedź na to pytanie jest prosta: Porzućcie wszelkie przypisanie, wy, którzy tu wchodzicie.
To jasne, że jeżeli wartość w miejscu w pamięci, raz zainicjowana, nie zmienia się podczas wykonywania programu, to nie ma niczego, o co te 131072 procesorów mogłoby rywalizować. Nie potrzebujesz semaforów, jeśli nie masz skutków ubocznych! Nie masz problemów współbieżnych aktualizacji (przepraszam: Jednoczesnych Aktualizacji), jeśli w ogóle nie masz aktualizacji!
Więc to jest ta wielka sprawa w językach funkcyjnych; i to jest naprawdę cholernie wielka sprawa. W naszym kierunku pędzi pociąg towarowy, załadowany po brzegi rdzeniami; i lepiej, żebyś był gotowy do czasu jego przyjazdu.



Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 22 grudnia 2012 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

11 lutego 2019

Konieczność TRYBU-B


Jeżeli śledzisz moje konta na twitterze, facebooku czy githubie, mogłeś zauważyć, że piszę emulator PDP-8 na iPada.
Oto screenshot:


Poniższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 21 lutego 2015 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.


Moim celem napisania tego emulatora (poza zwykłą nostalgią) jest wykorzystanie go jako narzędzia szkoleniowego dla nowych programistów. Myślę, że każdy świeżo upieczony programista powinien spędzić tydzień lub dwa programując na jednej z tych starych maszyn. Wydaje mi się, że nie ma lepszego sposobu, aby zrozumieć, czym komputer tak naprawdę jest, jak tylko móc dotknąć prawdziwego komputera i móc zaprogramować go na poziomie bitów, w języku maszynowym. Jak tylko to zrobisz, cała magia zniknie i zostanie zastąpiona przez twardą, brutalną rzeczywistość. I, coś Ci powiem, programowanie PDP-8 jest twardą, brutalną, rzeczywistością. A niech mnie, jak cholera!
Chciałem być wierny maszynie i jej środowisku. Panel przedni jest przyzwoitą abstrakcyjną reprezentacją oryginalnego PDP-8, i światełka zapalają się odpowiednio, i z prawidłowymi danymi (chociaż nie mogłem się oprzeć i zrobiłem światełka wrażliwe na dotyk, tak jak w ECP-18).

Papierowe taśmy w czytniku i dziurkacz poruszają się z odpowiednimi prędkościami i dziurki reprezentują prawdziwe dane. Wydają one także odpowiednie rodzaje dźwięków. Dalekopis drukuje z odpowiednią prędkością (chociaż możesz go przyspieszyć, jeśli chcesz (będziesz chciał)) i zachowuje się mniej więcej jak ASR-33, wydając odpowiednie dźwięki, i reagując odpowiednio na znaki powrotu karetki i wysuwu wiersza itd. (Tak, możesz naddrukować!)
Znalazłem jakieś binarne obrazy starego PDP-8 tutaj, i udało mi się wrzucić je do mojego emulatora, miażdżąc ich format przy użyciu małego programu w C, i wrzucając je na iPada, używając Dropboxa. Wynik tych działań był zarówno satysfakcjonujący, jak i łapiący za serce. Starożytny kod działa!
Usiadłem przy bezprzewodowej klawiaturze mojego 600-dolarowego, ćwierćkilogramowego, obudowanego w fajny pokrowiec iPada, otoczony podręcznikami programowania i poznaczonymi listingami programu, nad którym pracowałem. Uświadomiłem sobie wtedy, że pracuję nad kodem napisanym pół wieku temu, stworzonym przez mężczyzn i kobiety (prawdopodobnie w większości przez kobiety w tamtych czasach), którzy zakasywali rękawy, aby sprawić, żeby ich śmieszna maszyna działała. Maszyna, która ważyła 225 kg, była rozmiaru lodówki i kosztowała 20'000$ w roku 1967.
Czy ktokolwiek z tych ludzi mógłby kiedykolwiek przypuszczać, że ich kod będzie odpalany na podręcznym tablecie i będzie używany do szkolenia programistów w dwudziestym pierwszym wieku? Niektórzy - wielu - mogą jeszcze żyć. Ciekawe co pomyśleliby, gdyby wiedzieli.
Mój emulator napisany jest w Lua, przy użyciu frameworku Codea dla iPada. Lua to niezwykle wygodny język dla developmentu na iPada. Jest (wystarczająco) szybki, i Codea zawiera wspaniałe kontrolki graficzne, i prosty, acz nadal bardzo skuteczny framework do tworzenia wysoce interaktywnych programów pełnych animacji.
To załatwiło animację (i generowanie dźwięku) na przednim panelu, dalekopis, i czytnik/dziurkacz papieru za jednym zamachem.
Emulowanie bebechów PDP-8 staje się niezłym wyzwaniem odkąd Lua ma tylko jeden typ numeryczny: zmiennoprzecinkowy. Tworzenie 12-bitowej logiki przy użyciu li tylko matematyki zmiennoprzecinkowej, jest, hmmm, interesujące. Z drugiej strony, miałem niezłą frajdę z oglądania FOCAL(FORmula CALculator: języka podobnego do Basica) odpalonego na moim PDP-8, dokonującego obliczeń zmiennoprzecinkowych, używając operacji logicznych, które to z kolei wymyśliłem przy użyciu matematyki zmiennoprzecinkowej LUA. &ltuśmiech&gt Powinieneś był zobaczyć, jak te światełka mrugały!
Prędkość uruchomieniowa to około 4000 instrukcji na sekundę. Mimo że to tylko 1/7 prędkości PDP-8/S, było to całkiem imponujące jak na iPada wykonywującego język "interpretowanego byte-codu" taki jak Lua, emulującego 12-bitową logikę przy użyciu obliczeń zmiennoprzecinkowych! Nie spodziewałem się takiej szybkości. W zasadzie odpala cały ten stary software firmy DEC z sensowną prędkością. Nawet FOCAL działa wystarczająco szybko do obliczania pierwiastków kwadratowych w coś koło pół sekundy.
I, znowu, te mruganie światełek podczas kompilacji jest bardzo przyjemne. Tylko popatrz, a od razu odkryjesz, czym były inspirowane filmy science-fiction z lat 50-tych XX. wieku.

TRYB-B

Sprawienie, aby kompilator działał, było naprawdę bardzo łatwe. Prawdopodobnie spędziłem nad tym w całości 30 godzin; i to włącznie z uczeniem się Codea i Lua.
Proces developmentu pędził jak szalony. Edytor Lua w Codea dla iPada jest potężyny i intuicyjny (chociaż nie ma refaktoringów &ltchlip&gt). Pętla edycji/testowania trwała, może, 10 sekund. Mogłem dodać linie, czy dwie, odpalić appkę, zobaczyć efekt, wskoczyć z powrotem do edytora ot tak. To była przyjemne udogodnienie i miałem przy tym niezłą zabawę.
Oczywiście napisałem testy do trudniejszych kawałków. Napisałem mały framework testowy, tylko do tego celu, i dodałem przycisk TEST na panelu przednim emulatora, aby ułatwić sobie odpalanie tych testów. Kod emulacji sam w sobie byłby prawie niemożliwy do napisania, gdybym nie miał tych testów. I, oczywiście, korzystałem z dyscypliny TDD przy pisaniu tego kodu. Koniec końców powstało ponad 100 testów dla najróżniejszych instrukcji i zachowań komputera PDP-8.
Dla GUI, z drugiej strony (i było tam mnóstwo kodu GUI), testy były niepotrzebne (&ltoch&gt). Moje oczy były testami. Wiedziałem, co chcę zobaczyć, więc rozkręciłem 10-sekundową pętlę edytuj/testuj. Pisanie testów w stylu TDD byłoby okropnie trudne, i stałoby się całkowitą stratą czasu.
Z drugiej strony, to był nadal rytm TDD. Wiedziałem, co chcę zobaczyć na ekranie. To był mój test. Po prostu zmodyfikowałem kod, zanim test został spełniony. A więc nawet jeśli nie pisałem testów, czułem się tak, jakbym pisał - czułem się, jak podczas TDD.
Oczywiście to prawda, że nie miałem zautomatyzowanych testów regresji dla mojego GUI. Z drugiej strony upewnienie się, czy wszystko działa było absurdalnie proste. W ten sposób brak zautomatyzowanych testów GUI nie wpłynął na czas trwania mojej pętli.
Oczywiście, bez narzędzi do refaktoringu kod zrobił się trochę zabałaganiony. Zrefaktorowałem to, czego nie mogłem znieść; ale rozmiar zabałagacenia jest większy, niżbym chciał. Uda mi się później posprzątać ten kod; ale dużo wolniej pracuje się bez dobrych narzędzi do refaktoringu.
Tym nie mniej, na potrzeby tego artykułu, nazwijmy ten styl programowania: TRYB-B. TRYB-B jest stylem, który pozwala Ci jednocześnie edytować coś na ekranie i widzieć na tym ekranie skutki, lub test, który przechodzi, w ciągu kilku sekund. To development o prędkości światła, który nie wymaga listingów, ołówków, czasu kompilacji, czasu poświęconego na ustawienie, lub żadnego innego utrudnienia. Czas pomiędzy edytowaniem kodu a oglądaniem go działającym jest dużo mniejszy niż jedna minuta.

TRYB-A

Mając działający Emulator PDP-8, i mając te wszystkie stare narzędzia takie, jak edytor taśmy papierowej, i działający assembler PAL-III, zabrałem się za pisanie prostego programu. Ten program pozwoliłby użytkownikowi wpisać prostą formułę na klawiaturze, i wtedy wyświetliłby wynik. Dla przykładu, jeśli użytkownik wpisałby: 25+32, komputer wyświetliłby 57.
Na PDP-8 nie jest to trywialny program. Zamieściłem go poniżej dla tych z Was, którzy chcieliby zobaczyć, jak biedni programiści PDP-8 musieli to pisać.
Proces był taki sam, jak proces, którego używałem w późnych latach 70-tych XX. wieku, kiedy pracowałem nad assemblerowymi programami na Teradyne M365 (18-bitowy kuzyn PDP-8). Mieliśmy taśmę magnetyczną, zamiast taśmy papierowej; i komputer był troszkę potężniejszy obliczeniowo niż PDP-8. Ale proces pozostawał taki sam. A leciało to tak:
Załóżmy, że jesteś w środku pisania tego programu z samego dołu. Już trochę napisałeś, i chcesz coś jeszcze dodać. Pamiętaj, ten komputer posiada słowa o długości tylko 4K. Nie może przechowywać wiele w swojej pamięci. Pamiętaj o tym, że jedynym magazynem pamięci masowej, którym dysponujesz, jest papierowa taśma. Także twój kod źródłowy jest na tym samym długim pasku tej samej papierowej taśmy.
  1. Zapisujesz zmiany, jakie chcesz wprowadzić do programu na aktualnym listingu. Będziesz miał zmiany na wielu stronach, więc wepnij spinacze biurowe na każdą ze stron, jeśli listing jest długi.
  2. Załaduj edytor z taśmy papierowej. To zajmie kilka minut, więc zrób sobie kawę.
  3. Ustaw przełączniki na panelu przednim na 6003: dla kompresji spacji, i użyj czytnika/dziurkacza wysokiej prędkości. Uruchom edytor (poprzez ustawienie 0200 na rejestrach PC i naciśnięcie klawisza RUN)
  4. Włóż papierową taśmę z twoim kodem źródłowym do czytnika
  5. Wczytaj jedną "stronę" kodu z papierowej taśmy używając komendy R. (50 linii lub mniej. 1 minuta lub więcej).
  6. Idź do tej strony w twoim listingu i nanieś poprawki, używając komend I, C i D. Pamiętaj, że nie masz ekranu, więc edytujesz linia po linii, używając numerów linii. Zaplanuj spędzić nad tym trochę czasu.
  7. Wydrukuj aktualną stronę, używając komendy L. Upewnij się, że wszystkie zmiany są poprawne.
  8. Wydziurkuj aktualną stronę na papierowej taśmie, używając komendy P. (minuta lub więcej).
  9. Wydziurkuj aktualną stronę i następnie ją wczytaj, używając komendy N i jeżeli to nie była ostatnia strona, idź do punktu 6.
  10. Wyjmij nową taśmę z kodem źródłowym z dziurkacza i oznacz ją tytułem i numerem wersji. Nigdy nie zapomnij o numerze wersji!
  11. Załaduj assembler do pamięci z papierowej taśmy (10 minut lub więcej).
  12. Ustaw przełącznik na panelu przednim na 2002: to konfiguracja "przejście pierwsze, wyjście na drukarkę".
  13. Załaduj źródłową taśmę do czytnika.
  14. Załaduj 0200 do rejestru PC, i wciśnij RUN.
  15. Kompilacja przejście pierwsze wczyta całą taśmę z kodem źródłowym i wydrukuje tablicę symboli. (10 minut albo więcej).
  16. Gdy komputer się zatrzyma, ustaw na panelu przednim przełączniki na 4003: konfiguracja "przejście drugie, wyjście na dziurkacz".
  17. Załaduj twój kod źródłowy do czytnika.
  18. Wciśnij RUN. Kompilacja przejście drugie wczyta całą twoją taśmę źródłową i wydziurkuje twoją binarną papierową taśmę. (15 minut albo więcej). Błędy w kodzie źródłowym wydrukują się podczas tego przejścia.
  19. Gdy komputer się zatrzyma, jeśli były jakieś błędy, wyrzuć papierową taśmę, która była przed chwilą dziurkowana i przejdź do punktu 1. Jeśli nie było, wyjmij taśmę binarną z dziurkacza i oznacz ją tytułem i numerem wersji. (Nie muszę Ci już przypominać o numerze wersji, prawda?)
  20. Ustaw przełączniki na panelu przednim na 6002: to konfiguracja "przejście trzecie, wyjście na drukarkę".
  21. Włóż twoją źródłową taśmę do czytnika.
  22. Wciśnij RUN. Kompilacja przejścia trzeciego wczyta całą taśmę z kodem źródłowym i wydrukuje listing programu. Będziesz potrzebował jej do debbugowania, więc nie zlekceważ tego. (30 minut albo więcej, ponieważ drukarka jest bardzo wolna). Upewnij się, że masz wystarczająco dużo papieru w drukarce!
  23. Oderwij listing i go sprawdź.
  24. Włóż swoją taśmę binarną do czytnika.
  25. Ustaw rejestry PC na 7777 (adres ładowarki binarnej, która zwykle jest przechowywana w pamięci rdzenia) i naciśnij RUN. Jeżeli w pamięci, z jakiegoś powodu, nie ma ładowarki binarnej, musisz przestawić się w tryb ładowarki RIM i wczytać ładowarkę binarną z papierowej taśmy przed tym krokiem.
  26. Gdy komputer się zatrzyma, twój program jest wczytany do pamięci. Uruchom go i zobacz, jak działa.
Ten proces jest bardzo skrócony. Jest tam jeszcze mnóstwo drobniejszych kroków, ale myślę, że masz już pełny obraz.
To jest TRYB-A. To jest bardzo delikatny, podatny na błędy proces, którego przeprowadzenie zajmuje godzinę lub więcej. Może być jeszcze dłużej dla większych programów. Bardzo mały program robi z tej pętli około 15 minut. Program, który pisałem, urósł do około 20-30 minut lub więcej, i oszukiwałem, pozwalając mojemu dalekopisowi działać z prędkością 10 razy większą niż normalna prędkość.
Żeby zmusić mój mały, śmieszny program do działania, szedłem tą pętlą siedem razy. Zajęło mi to około tydzień, i łącznie około pięć godzin. Wiele z tego było pisaniem kodu z użyciem ołówka, ponieważ bez edytora na ekranie, nie było możliwości ominąć ręcznego pisania, i używania gumki do ścierania bez przerwy.
W latach 70-tych spędzałem dni, tygodnie, i lata na pracy w TRYBIE-A. Wszyscy programiści spędzali. Tak właśnie wtedy wyglądało programowanie.
Jest jedna sprawa, jeśli chodzi o TRYB-A: MUSISZ BYĆ OSTROŻNY. Każdy błąd kosztuje cię godzinę lub więcej. Więc spędzasz mnóstwo czasu, ogarniając szczegóły, upewniając się, że kod jest dobry; że wyedytowałeś go odpowiednio; że przełączniki są ustawione właściwie; że taśmy są opisane poprawnie; etc
W TRYBIE-A nie bierzesz niczego za pewnik. Robisz wszystko rozmyślnie i ostrożnie. Ponieważ to jedyna droga, abyś robił to szybko. (Jeśli "szybko" jest w tym przypadku dobrym słowem.)
Nazwijmy tę rozwagę i ostrożność: zachowanie w TRYBIE-A.

TRYB-A kontra TRYB-B

TRYB-A jest o wiele wolniejszy niż TRYB-B. Czas pętli jest niemożliwie długi, zakres funkcjonalności, którą udaje Ci się stworzyć w ciągu każdej pętli, jest śmiesznie mały. Dla przykładu, podczas mojej pierwszej pętli w tym procesie napisałem i zdebbugowałem podprogram, który wczytywał linię tekstu z klawiatury, zakończonej znakiem CR ( Carriage Return - Powrót Karetki... Tak, dalekopis miał "karetkę", lub raczej "głowicę drukującą", którą można było zmusić do "powrotu".)
TRYB-B z kolei jest szybki! Naprawdę, naprawdę szybki. Czas pętli jest bardzo krótki, i możesz zrobić o wiele więcej w każdej pętli. Dla przykładu zajęło mi tylko kilka pętli, żeby zrobić poprawnie animację przechodzenia papierowej taśmy przez czytnik i dziurkacz. Każda maszynowa instrukcja PDP-8 zajęła mi pętlę lub dwie. Sprawienie, aby przesuwał się papier w TTY, zajęło mi dwie lub trzy pętle.
No i, oczywiście, nie używałem listingów. Nie pisałem kodu najpierw na papierze. Mogłem pójść gdziekolwiek, gdzie chciałem w programie i wyedytować dowolną linię, jaką chciałem w mgnieniu oka. Miałem podświetlanie składni, automatyczne wcięcia, znajdź i zamień, scrollowanie, zakładki, i dokumentację na Internecie.
Tryb-B jest szybki!

Konieczność TRYBU-B

Więc dlaczego jeszcze tak wielu programistów wciąż pracuje w TRYBIE-A? Robią to, wiecie. Wrzucają jedną kupę śmieci na drugą, i framework na framework, aż ich czas pętli rośnie z sekund do minut i dłużej. Wstrzykują tak wiele zależności, że ich buildy stają się kruche i podatne na błędy. Tworzą tak wiele niewyizolowanych zewnętrznych zależności, że mogliby równie dobrze używać papierowej taśmy. Jak ktokolwiek może robić cokolwiek, co może zwiększyć czas trwania tej pętli? Dlaczego nikt nie broni czasu swoich pętli z narażeniem życia?
Czy unikanie TRYBU-A nie jest naszą moralną powinnością? Czy nie powinniśmy robić wszystkiego, co tylko możliwe, aby utrzymać nasze cykle rozwoju oprogramowania w TRYBIE-B? Czy TRYB-B nie jest koniecznością?
Czy chcesz poznać sekret pozostawania w TRYBIE-B? Wiem, co to jest. Powiem Ci.
Sekretem pozostawania w TRYBIE-B jest wykorzystanie zachowania w TRYBIE-A.

A NIECH MNIE KULE METODOLOGII STRUKTURALNEJ BIJĄ!

Właśnie odkryłem, kto napisał assembler PAL-III na PDP-8. Czapki z głów. Był to Ed Yourdon.
Program dla PDP-8, który przyjmuje dwie liczby rozdzielone pojedynczym operatorem i wyświetla wynik:

           *20
0020  7563  MCR,    -215
0021  0212  KLF,    212
0022  7540  MSPC,   -240
0023  7520  MZERO,  -260
0024  7766  M10,    -12
0025  0276  PROMPT, 276 />
0026  0215  KCR,    215
0027  7525  MPLUS,  -253
0030  7523  MMINUS, -255
0031  0277  QMARK,  277
0032  0260  KZERO,  260

            /WORKING STORAGE
0033  0000  REM,    0

            /CALL SUBROUTINE IN ARG
0034  0000  CALL,   0
0035  3046          DCA AC
0036  1434          TAD I CALL
0037  3047          DCA CALLEE
0040  1034          TAD CALL
0041  7001          IAC
0042  3447          DCA I CALLEE
0043  2047          ISZ CALLEE
0044  1046          TAD AC
0045  5447          JMP I CALLEE
0046  0000  AC,     0
0047  0000  CALLEE, 0

----------------

            *200
            /CALC A+B OR A-B
            /MAIN LOOP: PROMPT, GET CMD, PRINT RESLT

0200  6046          TLS
0201  7200  IDLE,   CLA
0202  1026          TAD KCR
0203  4034          JMS CALL
0204  0425          PRTCHAR
0205  7200          CLA
0206  1025          TAD PROMPT
0207  4034          JMS CALL
0210  0425          PRTCHAR
0211  4034          JMS CALL
0212  0400          RDBUF
0213  2000          BUF
0214  4034          JMS CALL
0215  0462          SKPSPC
0216  2000          BUF
0217  3222          DCA .+3
0220  4034          JMS CALL
0221  0477          GETNUM
0222  0000          0
0223  3261          DCA A
0224  1622          TAD I .-2
0225  3263          DCA OP
0226  1222          TAD .-4
0227  7001          IAC
0230  3233          DCA .+3
0231  4034          JMS CALL
0232  0477          GETNUM
0233  0000          0
0234  3262          DCA B
0235  1263          TAD OP
0236  1027          TAD MPLUS
0237  7650          SNA CLA
0240  5254          JMP ADD
0241  1263          TAD OP
0242  1030          TAD MMINUS
0243  7650          SNA CLA
0244  5251          JMP SUB
0245  1031          TAD QMARK
0246  4034          JMS CALL
0247  0425          PRTCHAR
0250  5201          JMP IDLE

0251  1262  SUB,    TAD B
0252  7041          CIA
0253  7410          SKP
0254  1262  ADD,    TAD B
0255  1261          TAD A
0256  4034          JMS CALL
0257  0600          PRTNUM
0260  5201          JMP IDLE

0261  0000  A,      0
0262  0000  B,      0
0263  0000  OP,     0

----------------

            *400
            /READ A BUFFER UP TO A CR
0400  0000  RDBUF,  0
0401  7200          CLA
0402  1600          TAD I RDBUF
0403  2200          ISZ RDBUF
0404  3215          DCA BUFPTR
0405  4216  RDNXT,  JMS RDCHAR
0406  3615          DCA I BUFPTR
0407  1615          TAD I BUFPTR
0410  1020          TAD MCR
0411  7450          SNA
0412  5600          JMP I RDBUF
0413  2215          ISZ BUFPTR
0414  5205          JMP RDNXT
0415  0000  BUFPTR, 0

            /READ ONE CHAR
0416  0000  RDCHAR, 0
0417  7200          CLA
0420  6031          KSF
0421  5220          JMP .-1
0422  6036          KRB
0423  4225          JMS PRTCHAR
0424  5616          JMP     I RDCHAR

            /PRINT ONE CHAR
0425  0000  PRTCHAR,0
0426  6041          TSF
0427  5226          JMP .-1
0430  6046          TLS
0431  3245          DCA CH
0432  1245          TAD CH
0433  1020          TAD MCR
0434  7440          SZA
0435  5242          JMP RETCHR
0436  1021          TAD KLF
0437  6041          TSF
0440  5237          JMP .-1
0441  6046          TLS
0442  7200  RETCHR, CLA
0443  1245          TAD CH
0444  5625          JMP I PRTCHAR
0445  0000  CH,     0


            /PRT A BUFFER
0446  0000  PRTBUF, 0
0447  7200          CLA
0450  1646          TAD I PRTBUF
0451  2246          ISZ PRTBUF
0452  3215          DCA BUFPTR
0453  1615  PRTNXT, TAD I BUFPTR
0454  4225          JMS PRTCHAR
0455  2215          ISZ BUFPTR
0456  1020          TAD MCR
0457  7640          SZA CLA
0460  5253          JMP PRTNXT
0461  5646          JMP I PRTBUF

----------------

            /SKIP SPACES AC= FIRST NON-SPACE
0462  0000  SKPSPC, 0
0463  7200          CLA
0464  1662          TAD I SKPSPC
0465  2262          ISZ SKPSPC
0466  3215          DCA BUFPTR

0467  1615  NXTCHR, TAD I BUFPTR
0470  2215          ISZ BUFPTR
0471  1022          TAD MSPC
0472  7650          SNA CLA
0473  5267          JMP NXTCHR
0474  7240          CLA CMA
0475  1215          TAD BUFPTR
0476  5662          JMP I SKPSPC

            /GET DECIMAL NUMBER
0477  0000  GETNUM, 0
0500  7200          CLA
0501  3335          DCA NUMBER
0502  1677          TAD I GETNUM
0503  3215          DCA BUFPTR

0504  1615  NXTDIG, TAD I BUFPTR
0505  1023          TAD MZERO
0506  3334          DCA DIGIT
0507  1334          TAD DIGIT
0510  7710          SPA CLA
0511  5327          JMP NONUM
0512  1024          TAD M10
0513  1334          TAD DIGIT
0514  7700          SMA CLA
0515  5327          JMP NONUM
0516  1335          TAD NUMBER
0517  7100          CLL
0520  7006          RTL
0521  1335          TAD NUMBER
0522  7004          RAL
0523  1334          TAD DIGIT
0524  3335          DCA NUMBER
0525  2215          ISZ BUFPTR
0526  5304          JMP NXTDIG
0527  1215  NONUM,  TAD BUFPTR
0530  3677          DCA I GETNUM
0531  2277          ISZ GETNUM
0532  1335          TAD NUMBER
0533  5677          JMP I GETNUM
0534  0000  DIGIT,  0
0535  0000  NUMBER, 0

----------------

            /DIVIDE AC BY ARG
            /Q IN AC, R IN REM
0536  0000  DIV,    0
0537  3033          DCA REM
0540  1736          TAD I DIV
0541  2336          ISZ DIV
0542  7041          CIA
0543  3361          DCA MDVSOR
0544  3362          DCA QUOTNT
0545  1033          TAD REM
0546  1361  DIVLUP, TAD MDVSOR
0547  7510          SPA
0550  5353          JMP DIVDUN
0551  2362          ISZ QUOTNT
0552  5346          JMP DIVLUP
0553  7041  DIVDUN, CIA
0554  1361          TAD MDVSOR
0555  7041          CIA
0556  3033          DCA REM
0557  1362          TAD QUOTNT
0560  5736          JMP I DIV
0561  0000  MDVSOR, 0
0562  0000  QUOTNT, 0

----------------

                    *600
                    /PRINT NUMBER IN DECIMAL
                    DECIMAL
0600  0000  PRTNUM, 0
0601  4034          JMS CALL
0602  0536          DIV
0603  1750          1000
0604  4225          JMS PRTDIG
0605  7200          CLA
0606  1033          TAD REM
0607  4034          JMS CALL
0610  0536          DIV
0611  0144          100
0612  4225          JMS PRTDIG
0613  7200          CLA
0614  1033          TAD REM
0615  4034          JMS CALL
0616  0536          DIV
0617  0012          10
0620  4225          JMS PRTDIG
0621  7200          CLA
0622  1033          TAD REM
0623  4225          JMS PRTDIG
0624  5600          JMP I PRTNUM

            /PRINT A DIGIT IN AC
                    OCTAL
0625  0000  PRTDIG, 0
0626  1032          TAD KZERO
0627  4034          JMS CALL
0630  0425          PRTCHAR
0631  5625          JMP I PRTDIG

----------------

            *2000
2000  0000  BUF,0


Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 21 lutego 2015 ze strony:


Proszę o komentarze, jeżeli ta luźność jest zbyt daleko posunięta.

Podstawy Programowania Funkcyjnego Epizod 3

Czy wszystkie Zasady Się Zmieniają? Kiedy tylko zaczynamy używać nowego paradygmatu , porównujemy z nim na...