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.

4 lutego 2019

Fekofile


Dostałem dzisiaj ciekawego maila. Zawierał poniższy tekst opisujący, jak ktoś wysłał maila do swoich współpracowników o refaktoringu, który zrobił:
Po tym, jak wysłałem ten e-mail do moich kolegów, spotkałem się z następującą reakcją; 1 członek zespołu uważał, że to było lepsze niż poprzednio i 2 osoby uważały, że to było im trudniej zrozumieć. Z tych 2 osób, które uważały to za gorsze; 1 uważa, że powinienem cofnąć swoje zmiany, a druga jest w stanie znieść te zmiany, żebym już tylko się zamknął :-)

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


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


Tu jest e-mail, który im wysłał, pokazujący kod przed refaktoringiem i po refaktoringu.

Temat: FW: Refaktoring miesiąca


Cos pominąłem? Czy właśnie udało mi się zejść z 31 linii kodu do 2 linii kodu?

------- przed zmianami ------
public static string GetLtsCode(IventoryBinItem item)
{
    string ltsCode = null;
    if (!string.IsNullOrEmpty(item.ParentItemNo)) //child item
    {

        ltsCode = "PzT";
        var isNfoOrDisc = item.IsNfoItem || item.IsDiscontinuedItem;
        //if (isNfoOrDisc && item.ParentIsNfoOrDiscontinuedItem ||
        //    !isNfoOrDisc && !item.ParentIsNfoOrDiscontinuedItem)
        if (!item.ParentIsNfoOrDiscontinuedItem)
        {
            ltsCode = "PzT";
        }
        //else if (item.ParentIsNfoOrDiscontinuedItem && !isNfoOrDisc)
        else if (item.ParentIsNfoOrDiscontinuedItem) //always capture demand for children of nfo (april19 change)
        //and childitem is not, then mark it as regular, otehrwise both PzT
        {
            ltsCode = null;
        }
    }
    else //parent
    {
        ltsCode = null;
        if (item.IsNfoItem || item.IsDiscontinuedItem)
        {
            //april 19 change
            //ltsCode = "PzT";
            ltsCode = null;
        }

    }

    return ltsCode;
}
-----------------

---- po zmianach ------
public static string GetLtsCode(IventoryBinItem item)
{
    bool isPzT = item != null
              && !item.ParentItemNo.IsNullEmptyOrWhiteSpace()
              && !item.ParentIsNfoOrDiscontinuedItem;
    return isPzT ? "PzT" : null;
}
------------------

---- I później, po małym refaktoringu ----
public static string GetLtsCode(IventoryBinItem item)
{
    return IsPzT(item) ? "PzT" : null;
}

private static bool IsPzT(IventoryBinItem item)
{
    return item != null
       && !item.ParentItemNo.IsNullEmptyOrWhiteSpace()
       && !item.ParentIsNfoOrDiscontinuedItem;
}
------------------
Pozwól mi wyrazić się jasno. Pierwszy kod to gówno. Wielkie gówno. Mam na myśli naprawdę śmierdzącą, rozwodnioną i brązową kupę kupy. Wygląda, jakby wyszła z naprawdę chorego psa.


Byłem zdumiony, że ktokolwiek mógłby uważać, że to gówno było lepsze od całkiem niezłego refaktoringu. Wydaje się, że niektórzy ludzie po prostu lubią wąchać kupę. Nazywam ich Fekofilami.
Pomyślałem sobie, że może mają coś nie w porządku z nosami. Koniec końców, jeżeli na co dzień żyjesz w gównie, po jakimś czasie możesz już po prostu tego nie czuć. Więc zdecydowałem się wepchnąć ich nosy w tę śmierdzącą kupę poprzez prześledzenie tego zapachu w każdym z jego intensywnych i gryzących detali. Więc, wysłałem z powrotem do mojego przyjaciela następujący e-mail.

Przejdźmy przez cały ten poprzedni kod, naprawdę obwąchując go dookoła!
  1. Zaczynamy poprzez zainicjowanie zmiennej ltsCode nullem. W porządku.
  2. Wtedy napotykamy podwójne zaprzeczenie (jeśli nie null/pusty) i dwukierunkowość (item.ParentItemNo). Potrzeba komentarza, żeby to zrozumieć. Wydaje się, że jeżeli ParentItemNo elementu będzie nullem lub puste, to znaczy, że element nie ma rodzica. Jeżeli dodamy zaprzeczenie, wyrażenie if zdaje się mówić: "jeżeli to jest dziecko". Komentarz stara się o tym powiedzieć, ale gramatyka kodu jest zła. Byłoby ładniej, gdyby wyrażenie if miało postać if(isChild(item)) lub if(item.isChild()).
  3. W ciele wyrażenia if jesteśmy dzieckiem, więc ustawiamy ltsCode na PzT.
  4. Ignorujemy zakomentowany kod.
  5. Ustawiamy zmienną isNfoOrDisc na interesujące wyrażenie. Co dziwne, nie widzę żadnego miejsca, w którym ta zmienna byłaby używana. Kiedyś była używana w zakomentowanym kodzie, więc może ktoś po prostu zapomniał zakomentować także zmiennej, co?
  6. Zaprzeczenie w wyrażeniu if (łatwo przegapić ten fakt).
  7. Wyrażenie if sprawdza, czy rodzic nie jest nfo lub nie jest już kontynuowany. Jeżeli tak, ustaw ltsCode na PzT. Ale przecież ltsCode jest już ustawione na PzT. Może ktoś zapomniał zakomentować całego tego wyrażenia if. &ltA fe!&gt
  8. Jeszcze więcej zakomentowanego kodu. Fuj.
  9. Aaa! A teraz klauzula else "rodzica Nie nfo lub niekontynuowanego" wyrażenia if, którego zaprzeczenie wyrażenia boolean sprawdzaliśmy wcześniej. Czy programista nie wie, jak działa if/else? Dlaczego do diabła on sprawdza tautologię? Przecież, w klauzuli else, my już WIEMY, że rodzic jest nfo lub nie jest kontynuowany. Do licha.
  10. No i w klauzuli else ustawiamy ltsCode na null. Czekaj, czekaj ... Nie ustawialiśmy go kiedyś wcześniej już na null?
  11. (uff, uff, uff) ok, doszliśmy do else. Co my tu mamy? To jest else z podwójnego przeczenia wyrażenia if z punktu 2. wcześniej. Aaa, więc to tu mamy element bez rodzica. Ten komentarz //parent jest dziwny. Czy to oznacza, że element jest rodzicem? Skąd to mamy wiedzieć? Wszystko, co wiem, to to, że element nie posiada nie nullowego lub pustego numeru elementu rodzica. A więc co jest z komentarzem //parent? To kłamstwo albo jakiś kiepski żart. Wydaje mi się, że komentarze powinny być wykomentowane. (wrrrr).
  12. Ustawiamy ltsCode na null. Hmmmm. zobaczmy. Jaką miało wartość ltsCode, zanim ustawiliśmy je na null? Wszak, wydaje mi się, że już jest nullem! Czy jest jakaś ścieżka wśród tego okropnego programistycznego gniazda węży, która mogłaby ustawić ltsCode na cokolwiek różnego od nulla? Nie ma!
  13. a teraz ostateczny cios! Kolejne wyrażenie if, które sprawdza, czy element jest nfo czy nie kontynowany. I co to wyrażenie if robi, jeżeli tak jest? Ano, ustawia ltsCode na ... NULL. Oczywiście! NULL. Genialne!
Podsumowując. Ten kod to śmietnik. W całym znaczeniu tego słowa, bezdyskusyjny, prawdziwy burdel. Jest pełen niedokładnych i dwuznacznych komentarzy, niepotrzebnych zmiennych, bezcelowych wyrażeń if i naprawdę zawiłej logiki. A jaki był cel? Cel był taki:
zwróć PzT, jeżeli element istnieje i ma rodzica, który nie jest nfo lub jest niekontynuowany. W przeciwnym razie zwróć nulla.
LUB, ujmując to w prostym kodzie

 public static string GetLtsCode(IventoryBinItem item) {
  return IsPzT(item) ? "PzT" : null;
 }

 private static bool IsPzT(IventoryBinItem item) {
  return item != null
    && item.HasParent()
    && item.ParentIsNotnfoOrDiscontinuedItem;
 }
Wstydzę się każdego, kto uważa, że oryginalny kod jest łatwiejszy do zrozumienia.

Nie wiem, czy to do nich doszło. Ale nawet, jeśli po raz pierwszy uświadomili sobie, że w ich domu śmierdzi, to jest to kropla w morzu. Jest jeszcze wielu, wielu fekofili na tym świecie, którzy muszą cofnąć się krok w tył i wziąć głęboki wdech nosem.


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


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

28 stycznia 2019

Przeprosiny



Poniższy tekst jest luźnym tłumaczeniem wpisu Roberta Cecila "Wujka Boba" Martina z dnia 11 maja 2014 ze strony:

Będącego przeprosinami za tytuł starej wersji wpisu bloga ze strony:


Pierwsza wersja tego wpisu z bloga była nazwana Frameworkowa Chłosta. Spróbowałem porównać kod, który wykorzystuje jakiś framework do konkubin w haremie. Myślałem, że moje komentarze są sprytne i śmieszne; ale wiele ludzi uznało je za obraźliwe.
Szczerze przepraszam za tę obrazę, nie było to zamierzone. Złamałem prawo Dona Normana:
Jeśli myślisz, że coś jest sprytne i wyrafinowane, uważaj, to jest najprawdopodobniej twój brak umiaru.
Spróbuję uniknąć takich błędów w przyszłości. Doceniam rozmowę o nich i mam nadzieję, że będzie ona kontynuowana konstruktywnie.
Pozwólcie, że ujmę to w ten sposób. Mój oryginalny wpis naraził kobiety na zestaw naprawdę kiepskich żartów. Ani to nie było moim zamierzeniem. Ani nie zauważyłem, że to zrobiłem. Wskazano mi to później; i zgodziłem się z tym i przeprosiłem. Wierzę, że postąpiłem słusznie.
Wskazanie, że stałeś/aś się obiektem bardzo słabych żartów jest rzeczą honorową. Nie ma nic złego w tym, że wstałeś/aś i powiedziałeś/aś: "Nie chcę być traktowany/a w ten sposób".
Mam 50`000 followersów na Twitterze. Jeśli niezamierzenie popełnię głupi żart i nie poprawię go, inni mogą zadecydować, aby mnie naśladować. Nie chcę tego.
Więc do tych, którzy uważają, że ugiąłem się pod jakimś rodzajem presji, mówię: nie macie racji. Poprawiłem mój wpis, bo się myliłem.


Powyższy tekst jest luźnym tłumaczeniem wpisu Roberta Cecila "Wujka Boba" Martina z dnia 11 maja 2014 ze strony:

Będącego przeprosinami za tytuł starej wersji wpisu bloga ze strony:


Związani Z Frameworkiem[2]


Frameworki to potężne narzędzia. Bez nich bylibyśmy zgubieni. Ale istnieje koszt używania frameworków.

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


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


Pomyśl o Rails, czy Springu, czy JSF czy Hibernate. Pomyśl o tym, jak pisałbyś systemy webowe bez pomocy tych frameworków. To wyobrażenie jest przygnębiające. Byłoby wtedy za dużo małych, upierdliwych szczegółów, z którymi musiałbyś sobie radzić. To byłoby jak zbudowanie obwodu pamięci mnemonicznej przy użyciu kamiennych noży i niedźwiedziej skóry[1].
A więc radośnie łączymy nasz kod ściśle z frameworkami w oczekiwaniu tych wszystkich obiecanych zysków. Popełniamy przy tym błąd, który popełniło już tak wielu programistów przed nami. Związujemy się z frameworkiem.
Użycie frameworku wymaga znaczącego zaangażowania. Akceptując framework w swoim kodzie, tracisz kontrolę nad szczegółami, którymi zarządza framework. Oczywiście wydaje się to być dobre; i zwykle jest. Jednakże pułapka czyha tuż za rogiem; i jest ciężko spostrzec ją w porę. Zanim się zorientujesz, jesteś zaangażowany we wszelkiego rodzaju nienaturalne działania, dziedzicząc z klas bazowych frameworku, rezygnując z coraz większej ilości kontroli przepływu sterowania, kłaniając się coraz głębiej konwencjom frameworku, jego dziwactwom i ekscentryzmom.
I pomimo Twojego ogromnego zaangażowania zrobionego w kierunku frameworku, framework nie odwzajemnił Ci się żadnym zaangażowaniem w Twoim kierunku. Framework łatwo ewoluuje w każdym kierunku, jaki tylko zadowala jego autora. A kiedy to robi, zdajesz sobie sprawę, że będziesz musiał podążać za nim grzecznie, jak wierny szczeniak.
Przywiązywanie się do frameworku zachodzi zbyt często wśród członków zespołów programistycznych. Zaczynają z dzikim entuzjazmem i chętnie wiążą swój kod z frameworkiem, tylko po to, żeby dużo później, wraz z rozwojem projektu, przekonać się, że framework staje im na drodze.
To nie powinno być zaskakujące. Frameworki są pisane przez ludzi do rozwiązania konkretnych problemów, które tylko ci ludzie mają. Te problemy mogą być Twoje, ale one nie są Twoje. Ty masz inne problemy. W momencie, gdy Twoje problemy pokrywają się, framework może być niesamowicie pomocny. W momencie, gdy Twoje problemy są inne, framework może być ogromnym utrudnieniem.

Autorzy Frameworków

Frameworki są pisane przez ludzi, a ludzie mają swoje własne motywy. Jednym z takich motywów może być sprawienie, abyś używał ich frameworku. Jako autor frameworków w przeszłości, mogę Ci powiedzieć, że im więcej ludzi używa twoich frameworków, tym masz o sobie lepsze mniemanie. Duża część mojego dowartościowania samego siebie była związana z akceptacją moich frameworków przez innych ludzi.
Tutaj nie chcę wchodzić zbyt głęboko w psychoanalizę autorów frameworków. Autorzy frameworków nie są złymi ludźmi. W rzeczywistości, w przeważającej części, są bohaterami. Wielu bezinteresownie wypuszcza swój kod dla społeczności open source. Gdyby nie ci ludzie, nasze programistyczne żywota byłyby znacznie mniej przyjemne i produktywne.
Przedstawiłem tę kwestię autorów, ponieważ pomaga mi ona zrozumieć pewną mechanikę zależności pomiędzy autorami a użytkownikami frameworków.
Autorzy frameworków pokonają znaczne odległości, żeby tylko udało im się zagonić Cię do ich zagrody. Będą pisać artykuły i przemawiać na konferencjach. Dostarczą przykłady pokazujące, w jaki sposób możesz połączyć się ściśle, bardzo ściśle z ich kodem. Pokażą wszystkie korzyści, jakie ma takie ścisłe połączenie. Oni są przekonani, że ich kod pomoże Ci, i będą ciężko pracować, żeby Cię o tym przekonać.
To jest zupełnie normalne i w żaden sposób nie jest niehonorowe. Po prostu chcą Cię mieć w swojej zagrodzie.
Jednakże, kiedy już będziesz w ich zagrodzie, ich zainteresowanie Tobą zmienia się w pewien sposób. To jest zdjęcie jednego z popularnych autorów frameworków mówiącego jego użytkownikom, co myśli o niektórych ich obawach. (Tylko dla dorosłych)

Na Odległość Wyciągniętych Ramion

Przez te wszystkie lata, nabyłem zdrowego sceptyzmu dotyczącego frameworków. Mimo że przyznaję, że mogą być ekstremalnie użyteczne i zaoszczędzić ogrom czasu; również zdaję sobie sprawę, że to kosztuje. Czasami te koszty rzeczywiście mogą być bardzo wysokie.
Więc, moją strategią jest trzymać frameworki takie, jak Spring, Hibernate, czy Rails na odległość wyciągniętych ramion; za granicami architektonicznymi. Najwięcej korzyści mam z nich korzystając z nich w taki sposób; i dzięki temu mam nad nimi bezlitosną przewagę.


Ale nie pozwalam tym frameworkom podchodzić zbyt blisko. Nie oddaję im mojej niezależności. I pozwalam mackom ich kodu grzebać w wysokopoziomowych zasadach moich systemów. Mogą dotykać moich drugoplanowych podsystemów; ale trzymam je z dala od kluczowej logiki biznesowej. Wysokopoziomowe zasady moich systemów nigdy nie powinny być dotykane przez frameworki.
[1] Star Trek: The City on the Edge of Forever, Harlan Ellison, 1967
[2] Przeprosiny


Powyższy tekst jest luźnym tłumaczeniem wpisu bloga Roberta Cecila "Wujka Boba" Martina z dnia 11 maja 2014 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...