Słyszałem wiele razy, gdy inni programiści używają tego wyrażenia do „reklamowania” niektórych wzorców lub opracowywania najlepszych praktyk. Przez większość czasu ta fraza jest używana, gdy mówimy o korzyściach z programowania funkcjonalnego.
Sformułowanie „łatwy do uzasadnienia” zostało użyte w obecnym brzmieniu, bez żadnego wyjaśnienia ani próbki kodu. Dla mnie staje się to więc kolejnym słowem „buzz”, którego bardziej doświadczeni programiści używają w swoich rozmowach.
Pytanie: Czy możesz podać przykłady „Niełatwe do uzasadnienia”, aby można je porównać z przykładami „Łatwo uzasadnić”?
Odpowiedzi:
Moim zdaniem wyrażenie „łatwe do uzasadnienia” odnosi się do kodu, który łatwo „wykonać w głowie”.
Patrząc na fragment kodu, jeśli jest krótki, jasno napisany, z dobrymi nazwami i minimalną mutacją wartości, to mentalne przepracowanie tego, co robi kod, jest (względnie) łatwym zadaniem.
Długi kawałek kodu ze złymi nazwami, zmiennymi, które stale zmieniają wartość i zawiłe rozgałęzienia zwykle wymagają np. Długopisu i kartki papieru, aby śledzić aktualny stan. Taki kod nie może więc być łatwo przetworzony tylko w twojej głowie, więc nie jest łatwo go zrozumieć.
źródło
"Code easy to reason about" almost exclusively alludes to its mathematical properties and formal verification
- to z grubsza brzmi jak odpowiedź na pytanie. Możesz opublikować to jako odpowiedź zamiast nie zgadzać się na temat (subiektywnej) odpowiedzi w komentarzach.Łatwo jest uzasadnić mechanizm lub fragment kodu, kiedy trzeba wziąć pod uwagę kilka rzeczy, aby przewidzieć, co zrobi, a rzeczy, które należy wziąć pod uwagę, są łatwo dostępne.
Prawdziwe funkcje bez efektów ubocznych i bez stanu są łatwe do uzasadnienia, ponieważ wynik jest całkowicie determinowany przez dane wejściowe, które są dokładnie tam w parametrach.
I odwrotnie, obiekt ze stanem jest znacznie trudniejszy do uzasadnienia, ponieważ musisz wziąć pod uwagę stan, w jakim znajduje się obiekt, gdy wywoływana jest metoda, co oznacza, że musisz pomyśleć o tym, w jakich innych sytuacjach obiekt mógłby znajdować się w szczególny stan.
Jeszcze gorzej są zmienne globalne: aby zrozumieć kod, który odczytuje zmienną globalną, musisz zrozumieć, gdzie w kodzie ta zmienna może być ustawiona i dlaczego - a znalezienie tych wszystkich miejsc może nie być łatwe.
Prawie najtrudniejszą rzeczą do uzasadnienia jest programowanie wielowątkowe ze stanem współdzielonym, ponieważ nie tylko masz stan, masz wiele wątków zmieniających go w tym samym czasie, aby zrozumieć, co robi kawałek kodu, gdy jest wykonywany przez jeden wątek trzeba dopuścić możliwość, że w każdym punkcie wykonania jakiś inny wątek (lub kilka z nich!) może wykonywać prawie każdą inną część kodu i zmieniać dane, nad którymi operujesz, pod twoimi oczami. Teoretycznie można temu zaradzić za pomocą muteksów / monitorów / sekcji krytycznych / jakkolwiek to nazwiesz, ale w praktyce żaden zwykły człowiek nie jest w stanie tego zrobić niezawodnie, chyba że drastycznie ograniczą państwo wspólne i / lub paralelizm do bardzo małych sekcje kodu.
źródło
make
a nawet specjalizacja szablonów C ++ i przeciążenie funkcji) może przywrócić cię do ponownego rozważenia całego programu. Nawet gdy myślisz, że znalazłeś definicję czegoś, język pozwala na bardziej szczegółowe określenie w dowolnym miejscu programu, aby to zastąpić. Twoje IDE może w tym pomóc.sealed
nie było to ustawienie domyślne?W przypadku programowania funkcjonalnego znaczenie „łatwego do przemyślenia” jest przede wszystkim deterministyczne. Rozumiałem przez to, że dane wejście zawsze prowadzi do tego samego wyjścia. Możesz robić, co tylko chcesz programowi, dopóki nie dotkniesz tego fragmentu kodu, nie złamie się.
Z drugiej strony, OO jest zazwyczaj trudniejsze do uzasadnienia, ponieważ wytworzony „wynik” zależy od stanu wewnętrznego każdego zaangażowanego obiektu. Typowy sposób, w jaki się manifestuje, to nieoczekiwane skutki uboczne : przy zmianie jednej części kodu część pozornie niezwiązana zrywa się.
... wadą programowania funkcjonalnego jest oczywiście to, że w praktyce wiele rzeczy, które chcesz zrobić, to IO i zarządzanie stanem.
Istnieje jednak wiele innych rzeczy, o których trudniej jest uzasadnić, i zgadzam się z @Kilian, że współbieżność jest doskonałym przykładem. Systemy rozproszone też.
źródło
Unikanie szerszej dyskusji i odpowiedź na konkretne pytanie:
Odsyłam cię do „Historii Mela, prawdziwego programisty” , kawałka folkloru programisty, który pochodzi z 1983 roku i dlatego dla naszego zawodu liczy się jako „legenda”.
Opowiada historię programisty piszącego kod, który w miarę możliwości preferował tajemne techniki, w tym kod samoreferencyjny i samodmodyfikujący oraz celowe wykorzystanie błędów maszynowych:
To jest przykład kodu, którego „trudno uzasadnić”.
Oczywiście Mel nie zgodzi się ...
źródło
Mogę podać przykład i bardzo powszechny.
Rozważ następujący kod C #.
Teraz rozważ tę alternatywę.
W drugim przykładzie dokładnie wiem, co ten kod robi na pierwszy rzut oka. Kiedy widzę
Select
, wiem, że lista przedmiotów jest konwertowana na listę czegoś innego. Kiedy widzęWhere
, wiem, że niektóre elementy są filtrowane. Na pierwszy rzut oka mogę zrozumieć, co tonames
jest i skutecznie z niego korzystać.Kiedy widzę
for
pętlę, nie mam pojęcia, co się z nią dzieje, dopóki nie przeczytam kodu. I czasami muszę to prześledzić, aby mieć pewność, że uwzględniłem wszystkie działania niepożądane. Muszę trochę popracować, aby nawet zrozumieć, jakie są nazwy (poza definicją typu) i jak je skutecznie wykorzystać. Zatem pierwszy przykład jest trudniejszy do uzasadnienia niż drugi.Ostatecznie łatwość rozumowania w tym przypadku zależy również od zrozumienia metod LINQ
Select
iWhere
. Jeśli ich nie znasz, to na początku trudniej jest zrozumieć drugi kod. Ale płacisz tylko za ich zrozumienie. Płacisz za zrozumieniefor
pętli za każdym razem, gdy używasz pętli za każdym razem, gdy się zmienia. Czasami warto zapłacić, ale zwykle ważniejsze jest „łatwiejsze uzasadnienie”.źródło
Powiązaną frazą jest (I parafrazę),
Przykładem stosunkowo „łatwego do uzasadnienia” może być RAII .
Innym przykładem może być unikanie śmiertelnego uścisku : jeśli możesz przytrzymać zamek i zdobyć inny zamek, a jest wiele zamków, trudno jest upewnić się, że nie ma scenariusza, w którym mogłoby dojść do śmiertelnego uścisku. Dodanie reguły typu „istnieje tylko jedna (globalna) blokada” lub „nie można uzyskać drugiej blokady, gdy trzymasz pierwszą blokadę”, sprawia, że system jest stosunkowo łatwy do uzasadnienia.
źródło
CComPtr<>
) z funkcją w stylu C (CoUninitialize()
). Uważam to również za dziwny przykład, o ile pamiętam, że wywołujesz CoInitialize / CoUninitialize w zakresie modułu i przez cały okres jego użytkowania, np. Wmain
lub wDllMain
, a nie w jakimś krótkim, krótkotrwałym zakresie funkcji lokalnej, jak pokazano w przykładzie .main
) dla aplikacji. Inicjujesz COM podczas uruchamiania, a następnie dezinicjujesz go tuż przed wyjściem. Tyle że masz obiekty globalne, takie jak inteligentne wskaźniki COM, używając paradygmatu RAII. Jeśli chodzi o mieszanie stylów: globalny obiekt, który zainicjował COM w swoim ctor i niezainicjowany w swoim dtor, jest wykonalny, a to, co sugeruje Raymond, jest subtelne i niełatwe do uzasadnienia.Istotą programowania jest analiza przypadków. Alan Perlis zauważył o tym w Epigramie nr 32: Programiści nie powinni być mierzeni ich pomysłowością i logiką, ale kompletnością analizy przypadków.
Łatwo jest uzasadnić sytuację, jeśli analiza przypadku jest łatwa. Oznacza to albo, że jest kilka przypadków do rozważenia, albo, w przypadku braku tego, kilka przypadków specjalnych - mogą istnieć duże przestrzenie przypadków, ale które się zawalają z powodu pewnych regularności lub ulegają technice rozumowania, takiej jak indukcja.
Na przykład rekurencyjna wersja algorytmu jest zwykle łatwiejsza do uzasadnienia niż wersja imperatywna, ponieważ nie przyczynia się do zbędnych przypadków, które powstają w wyniku mutacji zmiennych stanu pomocniczego, które nie pojawiają się w wersji rekurencyjnej. Ponadto struktura rekurencji jest taka, że wpasowuje się ona w matematyczny wzór korektora przez indukcję. Nie musimy brać pod uwagę złożoności, takich jak warianty pętli i najsłabsze ścisłe warunki wstępne, i tak dalej.
Innym aspektem tego jest struktura przestrzeni obudowy. Łatwiej jest uzasadnić sytuację, która ma płaski lub przeważnie płaski podział na sprawy w porównaniu z sytuacją zhierarchizowaną: sprawy z pod-sprawami i pod-sprawami i tak dalej.
Właściwością systemów, która upraszcza rozumowanie, jest ortogonalność : jest to właściwość polegająca na tym, że przypadki rządzące podsystemami pozostają niezależne po połączeniu tych podsystemów. Żadne kombinacje nie powodują powstania „specjalnych przypadków”. Jeśli coś z czterema przypadkami jest połączone z trzema przypadkami coś prostopadle, istnieje dwanaście przypadków, ale idealniekażdy przypadek jest połączeniem dwóch przypadków, które pozostają niezależne. W pewnym sensie tak naprawdę nie ma dwunastu przypadków; kombinacje to po prostu „pojawiające się zjawiska podobne do przypadków”, o które nie musimy się martwić. Oznacza to, że nadal mamy cztery przypadki, o których możemy pomyśleć bez rozważenia pozostałych trzech w innym podsystemie i odwrotnie. Jeśli niektóre kombinacje muszą być specjalnie zidentyfikowane i wyposażone w dodatkową logikę, wówczas rozumowanie jest trudniejsze. W najgorszym przypadku każda kombinacja ma specjalne podejście, a następnie naprawdę jest dwanaście nowych przypadków, które są dodatkiem do oryginalnych czterech i trzech.
źródło
Pewnie. Weź współbieżność:
Sekcje krytyczne egzekwowane przez muteksy: łatwe do zrozumienia, ponieważ istnieje tylko jedna zasada (dwa wątki wykonania nie mogą wejść jednocześnie do sekcji krytycznej), ale podatne zarówno na nieefektywność, jak i impas.
Alternatywne modele, np. Programowanie bez blokady lub aktorzy: potencjalnie znacznie bardziej eleganckie i potężne, ale piekielnie trudne do zrozumienia, ponieważ nie można już polegać na (pozornie) podstawowych pojęciach, takich jak „teraz napisz tę wartość do tego miejsca”.
Łatwość uzasadnienia jest jednym z aspektów metody. Ale wybór metody, którą należy zastosować, wymaga uwzględnienia wszystkich aspektów łącznie .
źródło
Ograniczmy to zadanie do formalnego uzasadnienia. Ponieważ humorystyczne, wynalazcze lub poetyckie rozumowanie ma inne prawa.
Mimo to wyrażenie jest niejasno zdefiniowane i nie można go ustawić ściśle. Ale to nie znaczy, że powinno być dla nas tak słabe. Wyobraźmy sobie, że struktura przechodzi test i otrzymuje oceny dla różnych punktów. Dobre oceny za KAŻDY punkt oznaczają, że konstrukcja jest wygodna pod każdym względem, a zatem „łatwa do uzasadnienia”.
Struktura „Łatwa do uzasadnienia” powinna uzyskać dobre oceny za:
Czy test jest subiektywny? Tak, oczywiście, że tak. Ale samo wyrażenie jest również subiektywne. To, co jest łatwe dla jednej osoby, nie jest łatwe dla innej. Tak więc testy powinny być różne dla różnych domen.
źródło
Pomysł, że możliwe jest rozumienie języków funkcjonalnych, wywodzi się z ich historii, zwłaszcza ML, który został opracowany jako język programowania analogiczny do konstrukcji, których logika funkcji obliczeniowych używała do rozumowania. Większość języków funkcjonalnych jest bliższa formalnym rachunkom programowania niż języki rozkazujące, więc tłumaczenie z kodu na wejście systemu rozumowania jest mniej uciążliwe.
Na przykład systemu rozumowania, w rachunku różniczkowym i całkowym, każda zmienna lokalizacja pamięci w języku imperatywnym musi być reprezentowana jako oddzielny równoległy proces, podczas gdy sekwencja operacji funkcjonalnych jest pojedynczym procesem. Czterdzieści lat później od prowokatora twierdzeń LFC pracujemy z GB pamięci RAM, więc posiadanie setek procesów jest mniejszym problemem - użyłem rachunku pi do usunięcia potencjalnych zakleszczeń z kilkuset wierszy C ++, mimo że reprezentacja ma setki procesy powodujące wyczerpanie przestrzeni stanu przez około 3 GB i wyleczenie sporadycznego błędu. Byłoby to niemożliwe w latach 70. lub wymagało superkomputera na początku lat 90., podczas gdy przestrzeń stanowa funkcjonalnego programu językowego o podobnej wielkości była wystarczająco mała, aby o tym wtedy myśleć.
Z pozostałych odpowiedzi zdanie to staje się gwarem, mimo że znaczna trudność, która utrudniała rozumienie języków imperatywnych, jest niszczona przez prawo Moore'a.
źródło
Łatwo uzasadnić to pojęcie specyficzne kulturowo, dlatego tak trudno jest wymyślić konkretne przykłady. Jest to termin zakotwiczony w ludziach, którzy mają rozumować.
„Łatwy do uzasadnienia” to tak naprawdę bardzo opisowe zdanie. Jeśli ktoś patrzy na kod i chce zrozumieć, co robi, to łatwo =)
Okej, rozkładam to. Jeśli patrzysz na kod, zwykle chcesz, żeby coś zrobił. Chcesz się upewnić, że robi to, co Twoim zdaniem powinno. Opracowujesz teorie na temat tego, co powinien robić kod, a następnie zastanawiasz się nad tym, aby spróbować argumentować, dlaczego kod rzeczywiście działa. Próbujesz myśleć o kodzie jak człowiek (a nie jak komputer) i próbujesz zracjonalizować argumenty dotyczące tego, co potrafi kod.
Najgorszym przypadkiem „łatwego do uzasadnienia” jest sytuacja, w której jedynym sposobem na zrozumienie tego, co robi kod, jest przechodzenie przez wiersz po wierszu jak maszyna Turinga dla wszystkich danych wejściowych. W takim przypadku jedynym sposobem na uzasadnienie czegokolwiek w kodzie jest przekształcenie się w komputer i wykonanie go w głowie. Te najgorsze przykłady można łatwo zobaczyć w zaciemnionych konkursach programistycznych, takich jak 3 linie PERL, które odszyfrowują RSA:
Dla łatwości rozumowania, termin ten jest bardzo kulturalny. Musisz wziąć pod uwagę:
Każdy z nich wpływa inaczej na „łatwy do uzasadnienia”. Weź przykład umiejętności rozumującego. Kiedy zaczynałem w mojej firmie, zalecano mi rozwijanie skryptów w MATLAB-ie, ponieważ „łatwo jest to uzasadnić”. Dlaczego? Cóż, wszyscy w firmie znali MATLAB. Gdybym wybrał inny język, każdemu byłoby trudniej mnie zrozumieć. Nieważne, że czytelność MATLAB jest okropna w przypadku niektórych zadań, po prostu dlatego, że nie została dla nich zaprojektowana. Później, w miarę rozwoju mojej kariery, Python stał się coraz bardziej popularny. Nagle kod MATLAB stał się „trudny do uzasadnienia”, a Python był językiem preferowanym do pisania kodu, który był łatwy do uzasadnienia.
Zastanów się także, jakie treści może mieć czytelnik. Jeśli możesz polegać na czytniku, który rozpoznaje FFT w określonej składni, „łatwiej jest uzasadnić” kod, jeśli trzymasz się tej składni. Pozwala im spojrzeć na plik tekstowy jako płótno, na którym namalowałeś FFT, bez konieczności wchodzenia w szczegółowe szczegóły. Jeśli używasz C ++, dowiedz się, jak bardzo czytelnicy czują się komfortowo z
std
biblioteką. Jak bardzo lubią programowanie funkcjonalne? Niektóre idiomy wychodzące z bibliotek kontenerów są bardzo zależne od preferowanego stylu idomatycznego.Ważne jest również zrozumienie, na jakie pytania czytelnik może być zainteresowany odpowiedzią. Czy Twoi czytelnicy najbardziej interesują się powierzchownym zrozumieniem kodu, czy też szukają błędów głęboko w jelitach?
To, jak pewny musi być czytelnik, jest w rzeczywistości interesujące. W wielu przypadkach mgliste rozumowanie w rzeczywistości wystarcza, aby wyprowadzić produkt z domu. W innych przypadkach, takich jak oprogramowanie lotu FAA, czytelnik będzie chciał mieć uzasadnienie ironclad. Natknąłem się na przypadek, w którym argumentowałem za użyciem RAII do określonego zadania, ponieważ „Możesz to po prostu skonfigurować i zapomnieć o tym ... zrobi to dobrze”. Powiedziano mi, że się mylę. Ci, którzy zamierzali zastanowić się nad tym kodem, nie byli ludźmi, którzy „chcą zapomnieć o szczegółach”. Dla nich RAII było bardziej jak wiszący czad, zmuszając ich do myślenia o wszystkich rzeczach, które mogą się zdarzyć po opuszczeniu pola widzenia.
źródło