Podstawy Programowania Funkcyjnego Epizod 1

uncle bob label image

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.

Konieczność TRYBU-B

uncle bob label image
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.

Fekofile

uncle bob label image
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.

Wpisz adres e-mail, aby zasubskrybować powiadomienia o nowych postach: