Wątek przeniesiony 2023-04-19 18:04 z Nietuzinkowe tematy przez Riddle.

Dlaczego nie mówi się juniorom o return-early?

0

Dlaczego tak rzadko upomina się innych programistów o niższym doświadczeniu żeby trzymali się guard pattern'u?

Dla szybkiego rozjaśnienia o czym mówimy osobom które najpewniej robią już tak z automatu to główną funkcją guard patternu jest wczesne zakończenie bloku kodu.

Mam okazję przeglądać ostatnio tony kodu i widzę ciągle ten sam problem, brak stosowania tego jakże prostego wzorca jakim cudem ktoś go jeszcze nie używa? Czytam implementacje innych programistów i nagminnie zwracam uwagę że któryś warunek mógłbyć wyciągnięty na samą górę i natychmiastowo przerwać cały processing (a nawet do niego nie dopuszczać). Aż mnie skręca jak widzę takie coś prawie że na samym końcu metody...

2

Czasem jest to objaw złego przemyślenia modu. Jakbyś miał monade i railway programming, to bys nie potrzebował takich "wzorców"...

1

Chodzi Ci o "return early" vs. "one return"?

5

Ja już jestem starym programista, a nie znałem takiego pojęcia. Za to znam fail-fast :)

0
Riddle napisał(a):

Chodzi Ci o "return early" vs. "one return"?

Chodzi mi o return early

0

Kiedyś uczono że funkcja powinna mieć jeden return czy coś takiego, bo to podobno czytelniej lub było to wymyślone pisząc w jakiejś specyficznej technologii

i tak już zostało tym z większym xp

0

Od tego właśnie jest code review, żeby słusznie zwracać na to uwagę. Chyba, że to jakieś legacy to wtedy rip

4

Niestety masz filozofów, którzy nie lubią, jak jest więcej niż jeden return, albo więcej niż 0 break w pętli, bez zrozumienia, po co kiedyś istniały te zasady.

Ale podstawowa odpowiedź na pytanie jest prosta. Wyobraź sobie, jak głupi jest przeciętny programista. Następnie zauważ, że połowa programistów jest głupsza od niego.

Prawdopodobnie błędnie zakładasz, że ten twój kod przechodził jakikolwiek code review albo pair programming.

0

Ok, strzelam, że problem jest taki: jeśli guard block jest potrzebny to zazwyczaj wynika to z błędnego zaprojektowania aplikacji. Więc ludzie, którzy skonocili projekt aplikacji raczej się nie przejmowali problemem, który guard block rozwiązuje. W rezultacie tego typu konstrukcje zaczynają się pojawiać dopiero, kiedy do projektu dołączy ktoś spełniający dwa warunki: jest ogarnięty i chce mu się naprawiać kod zastany, co niekoniecznie musi się wydarzyć.

Druga sprawa jest taka, że - jak wspomniano - ludzie chcą czasem mieć single point of return. Zazwyczaj to ci, którzy doświadczyli traumy gdy zobaczyli kod ludzi, którzy potrafią pisać proceduralno-strukturalnie w językach obiektowych.

1
Pixello napisał(a):

Czasem jest to objaw złego przemyślenia modu. Jakbyś miał monade i railway programming, to bys nie potrzebował takich "wzorców"...

Czasami, być może da się ten kod przemyśleć lepiej, ale nie wiem co ma z tym wspólnego monada, skoro "pattern" służy do sprawdzenia czy parametry przekazane do funkcji należą do dziedziny funkcji, również tej przekazanej do monady.

dotgo napisał(a):

Dlaczego tak rzadko upomina się innych programistów o niższym doświadczeniu żeby trzymali się guard pattern'u?

Bo większość juniorów nie przejmuje się niczym poza ścieżką optymistyczną. W dalszej karierze ten nawyk też nie pojawia się wcale tak często. Mniejsza z tym, czy odpowiedź na parametry nie należące do dziedziny funkcji zostanie zwrócona wcześnie, czy późno, najpierw trzeba oganąć, że np. przez zero się nie dzieli.

Do tego, ktoś kiedyś napisał, że wiele return w ciele funkcji to źle, bo faktycznie funkcja na 300 linii zwracająca wynik w 15 przypadkowych miejscach jest mało czytelna. Problem w tym, że wprowadzenie lokalnej zmiennej result za bardzo tej czytelności nie zwiększa, a o tym, że rozwiązaniem jest podzielenie funkcji na mniejsze kawałki już mało kto doczytał. Skutek taki, że jest kolejna "religijna" zasada, której sens został zapomniany, ale można pokrzyczeć "Uncle Bob napisał, że to źle".

@Pixello: odpisuję tutaj na komentarz
Jak rozumiem, piszemy o takim "wzorcu":

fun divide(a:Float, b:Float):Float?{
  if(b == 0) return null //guard
  return a/b
}

Piszemy o ciele funkcji, tymczasem monada to pudełko, do którego tę funkcję wstrzykujemy (najczęściej jakąś lambdą) Nie rozumiem w jaki sposób użycie monady. Owszem, można sobie tutaj zmienić typ zwracany na jakieś Either, ale to nie ma znaczenia dla tego tematu, bo cała ta dyskusja jest o użyciu 2 return, z których pierwszy zabezpiecza przed wyjątkami mogącymi polecieć w kolejnych liniach.

0

Nie miałem pojęcia, że jest jeszcze jakaś inna nazwa na return early. Staram się stosować to podejście/technikę/wzorzec (jak zwał tak zwał) przy projektach, choć zdarzyło mi się rzucać wyjątek zamiast zwrać wynik.

0

Ten guard to nie tylko "return early" vs. "one return"? ale też dotyczy sposobu ułożenia przerwań. Pisanie pod guard będzie sprawiało, że kod obsługujący argumenty będzie miał rozwinięcie obsługi już na początku. Czyli to znaczy, że pisząc kod od razu trzeba wyliczać problemy, a dopiero pod sam koniec pisać logikę związaną z docelowo poprawnym przypadkiem.

Czyli to krzyżówka 2 tematów:

Temat 1: Odnośnie "return early" vs. "one return"? :

Moim zdaniem to podejście bardziej wymusza język.

Przy językach imperatywnych, zwłaszcza tych, które mają wyjątki nieodzowne jest poleganie na wielu instrukcjach przerwań. Nie spotkałem w życiu takiego przypadku, aby ktoś w imperatywnym języku zamiast pisać 3 throw w metodzie użył ifologię, by rozpoznać warunek, i pod każdy stworzyć osobny obiekt błędu, a potem rzucić go za pomocą jednego dostępnego throw w metodzie. Bez sensu jest robić dodatkowe referencję w takim przypadku. To już niepotrzebna akrobacja, a jeśli już to sytuacja specjalna, i być może wyszukana optymalizacja.

Przy językach funkcyjnych masz już ograniczenie nałożone z poziomu języka. Dla przykładu w clojure nie masz jawnie pisanego return. Po prostu ostatnia wyliczona wartość jest wynikiem. Takie wykonanie powoduje, że mogę chwycić fragment kodu i wykonać go w innym miejscu (które nie jest funkcją) np. w REPL i wykonać. Jawnie pisany return byłby w takiej sytuacji zbyt formalnym ograniczeniem. Clojure nie jest czysto funkcyjny, umożliwia rzucanie wyjątków więc nadal można korzystać z wielu throw, ale właściwie tutaj obliczenie typu błędu ma większy sens niż pisanie osobno trzech throw.

Temat 2: Co pierw? Optymistyczna ścieżka czy pesymistyczna?

Pesymistyczna ścieżka jest dobra do pisania kodu z efektami, a to dlatego, że samoczynnie zachęca (przynajmniej mnie), aby zastanowić się czy funkcja dałaby radę dalej pociągnąć pracę gdyby odpalono ją po jakiejś awarii, wtedy np. if przed wykonaniem sekcji sprawdza czy czegoś nie ma, bo jak nie ma to coś stworzy. Tu nie byłoby sensu tego jak przesuwać, bo dalsza część kodu bazuje na tym co przywrócił ten if.

Tam gdzie nie mam efektów to zawsze wybieram pozytywną ścieżkę, ponieważ nie znoszę podwójnej negacji, a taka zdarza się jeśli ktoś w już w samej nazwie zmiennej ma zaprzeczenie np. 'invalid' to zapis z tą zmienną na chwilę mnie przyblokuje w myślach. Pozytywne ścieżki najprzyjemniej pisze mi w czasie pisania rekurencji, wtedy kod można przeczytać dosłownie jak opis funkcji, wyrwany niczym ze słownika pojęć. Czy są w ogóle definicje w słowniku języka polskiego, które tłumaczą pojęcie poprzez zaprzeczenie? No właśnie.

Jak widać, krzyżując te dwa tematy, bądź rozpatrując je osobno, właściwie nie ma się wyboru.

1
piotrpo napisał(a):

@Pixello: odpisuję tutaj na komentarz
Jak rozumiem, piszemy o takim "wzorcu":

fun divide(a:Float, b:Float):Float?{
  if(b == 0) return null //guard
  return a/b
}

Piszemy o ciele funkcji, tymczasem monada to pudełko, do którego tę funkcję wstrzykujemy (najczęściej jakąś lambdą) Nie rozumiem w jaki sposób użycie monady. Owszem, można sobie tutaj zmienić typ zwracany na jakieś Either, ale to nie ma znaczenia dla tego tematu, bo cała ta dyskusja jest o użyciu 2 return, z których pierwszy zabezpiecza przed wyjątkami mogącymi polecieć w kolejnych liniach.

to zupełnie nie o to chodzi, tyle, że po prostu kotlin słabo wspiera monady, więc trudno pokazać. W Scali miałbyś kod

def doItBabe(a: Int) : Either<Error, String> 
   for {
      _ <- check(a)  //guard
      [...]
      user <- getUserFromDb(a)
      
    } yield (user.name)

w kotlinie to by było ( check(a).flatMap {_ -> getUserFromDb(a).map(it.name) } ).
Either/flatMap (i różne inne monady) robi Ci za short Circuit.

Tylko niestety dobrze jak język ma wsparcie w składni, inaczej te flatMapy średnio wyglądają.
Ale technicznie to w sumie najfajniejsza opcja, jedno wyrażenie, jeden return, sprawdzanie typów. A do tego masz zalety early returna.

Btw: W zasadzie to masz wsparcie dla 2 "prawie monad" w kotlinie:
?.let to flatMap na "Optionalu", a korutyny tworzą coś w stylu IO.
Twój przykład można zapisać by:

fun divide(a:Float, b:Float):Float? = 
  nonZero(b).? {a/b}

(poza tym i tak widać sztukę dla sztuki -- mamy mała funkcję więc pato patterny i tak się nie sprawdzają (i bardzo dobrze)).

0

Czyli jak rozumiem, składnia, którą opisałeś to coś na kształt fun either(condition, supplierIfTrue, supplierIfFalse):Either?

1
jarekr000000 napisał(a):

to zupełnie nie o to chodzi, tyle, że po prostu kotlin słabo wspiera monady, więc trudno pokazać. W Scali miałbyś kod

def doItBabe(a: Int) : Either<Error, String> 
   for {
      _ <- check(a)  //guard
      [...]
      user <- getUserFromDb(a)
      
    } yield (user.name)

w kotlinie to by było ( check(a).flatMap {_ -> getUserFromDb(a).map(it.name) } ).
Either/flatMap (i różne inne monady) robi Ci za shortCirtuit.

No, ale czym koncepcyjnie ten kod różni się od tego Javowego poza inną składnią i jak to ma się do dyskusji, że dzięki zastosowaniu monady nie trzeba mieć wzorca "guard"? Ja tu dalej widzę guarda tylko zaimplementowanego w inny sposób.

0
some_ONE napisał(a):

jak to ma się do dyskusji, że dzięki zastosowaniu monady nie trzeba mieć wzorca "guard"?

Dzięki monadzie nie trzeba mieć dwóch returnów.

"guard" dalej występuje tylko w Haskellu/FP nazywa się to short circuit. Bo w Haskellu guards to taki fikuśny case. W zasadzie w Haskellu jak nie trzeba wyników częściowych z walidacji to też można zrobić obsługę błędu na guardsach

factorial :: Integer -> Either String Integer
factorial n 
  | n < 0     => Left "number lower then 0"
  | otherwise => Right (factorialUnsafe n)

co by się przekłądało na Scalę trochę brzydziej:

def factorial(n: Int): Either[String, Int] = () match {
  case _ if n < 0 => Left("number lower then 0")
  case _          => Right(factorialUnsafe (n))
}

Też jeden domyślny return

UPDATE
No, ale jak ktoś nie lubi FP to nie po to wymyślano w OOP exceptiony żeby rzeźbić wiele returnów :P
W stylu obiektowym można napisac jeszcze w Scali tak:

def factorial(n: Int): Int = {
  require (0 < n, "Number lower then 0")
  factorialUnsafe(n)
}

rzucanie exceptionów opakowane w osobną funkcję/metodę. Jak w Javie. Z tą różnicą że w Javie trzeba użyć Guavy, a w Scali jest to w bibliotece standardowej.
I na to programiści OOP chyba mówią "programowanie defencywne"? Nie wiem, nie umiem w OOP

1
some_ONE napisał(a):

No, ale czym koncepcyjnie ten kod różni się od tego Javowego poza inną składnią i jak to ma się do dyskusji, że dzięki zastosowaniu monady nie trzeba mieć wzorca "guard"? Ja tu dalej widzę guarda tylko zaimplementowanego w inny sposób.

Bardzo dobre pytanie.

Odpowiedź: principle of least power.

Return (ogólny) to narzędzie dość poteżne i pozwala przypadkiem narobić szkód. Wystarczy, że taki early return bedzie zrobiony nie za early i już możemy mieć jakieś cieknące zasoby. Do tego robimy pole do dyskusji czy coś jest guardem czy po prostu niechlujnym multiple return.

Monada to po prostu dużo bardziej ograniczone rozwiązanie, w efekcie ma więsze szanse na łagodne przeżycie refaktoringôw.

2
KamilAdam napisał(a):

rzucanie exceptionów opakowane w osobną funkcję/metodę. Jak w Javie. Z tą różnicą że w Javie trzeba użyć Guavy, a w Scali jest to w bibliotece standardowej.
I na to programiści OOP chyba mówią "programowanie defencywne"? Nie wiem, nie umiem w OOP

A gdzie w definicji programowania defensywnego jest mowa o jakimś OOP albo wyjątkach?

Co zaś do samego guard pattern - też bym żadnemu juniorowi o tym nie powiedział, bo nie wiedziałem, że weryfikacja danych wejściowych do metody to jakiś wzorzec.

2

A nie zeszliśmy z tą dyskusją w stronę pisania o mało istotnych szczegółach składniowych? Bo chyba wszyscy się zgadzamy, że przypadek w którym np. parametry wejściowe wymuszają dzielenie przez zero powinien być "jakoś" obsłużony. Jasne, że Either jest bardziej elegancki od return or throw, ale też nie jest niczym więcej jak nieco wygodniejszym w dalszym użyciu pudełkiem na wartość/wyjątek.

0
piotrpo napisał(a):

A nie zeszliśmy z tą dyskusją w stronę pisania o mało istotnych szczegółach składniowych? Bo chyba wszyscy się zgadzamy, że przypadek w którym np. parametry wejściowe wymuszają dzielenie przez zero powinien być "jakoś" obsłużony. Jasne, że Either jest bardziej elegancki od return or throw, ale też nie jest niczym więcej jak nieco wygodniejszym w dalszym użyciu pudełkiem na wartość/wyjątek.

No ale o czym tu można dyskutować skoro wszyscy się zgadzają że sprawdzać trzeba? Wątek do zamknięcia chociaż takie nie było pierwotne pytanie OPa :D

A odpowiadając na pierwotne pytanie OPa. Jakbym dostał w swoje ręce juniora to nie o takich rzeczach bym mu opowiedział XD Może pytanie powinno brzmieć:

  1. Czemu na studiach nie ma tego w materiale nauczania?
  2. Czemu juniorzy nie czytają książek i wszystko trzeba im mówić? Co, mam im opowiedzieć 300 stron książki, bo nie mają czasu poczytać?

Na żadne z tych pytań nie jestem w stanie odpowiedzieć. Jak pewnie wszyscy tu. To chociaż pochwalmy się jak najładniej zaimplementować strażnika w swoim ulubionym języku programowania :P

2

To chociaż pochwalmy się jak najładniej zaimplementować strażnika w swoim ulubionym języku programowania :P - Jak rozpoznać programistę piszącego funkcyjnie? Sam Ci o tym powie a tak mi się skojarzyło :P

0

Dobre XD Chociaż akurat guards w Haskellu albo Pattern Matching z ifem ze Scali nie są funkcyjne i równie dobrze mogby by zostać zaimplementowane w dowolnym języku. No ale jak w Javie tyle się wożą z zwykłym Pattern Matching to co ja jeszcze ifów w tym będę wymagać XD Myślę że swiat byłby lepszym miejscem jakby imperatywne C miało zaimplementowane guards z Haskella :P

2

Nie wiem jak w ogóle możecie wspominać o wyjątkach jako alternatywie? Przecież wyjątki są do sytuacji wyjątkowych, nie do walidacji danych 😅.

Poza tym wyjątki są cholernie wolne - chyba że w javie jest inaczej?

3
Pixello napisał(a):

Nie wiem jak w ogóle możecie wspominać o wyjątkach jako alternatywie? Przecież wyjątki są do sytuacji wyjątkowych, nie do walidacji danych 😅.

Wyjątki to po prostu element języka stosowany do kontroli przepływu, taki sam jak if czy for.

Dodawanie do niego sentymentalnych konotacji: "te zapisy są do tego, a te do tego" to jakieś nie merytoryczne rozważania. To jest droga do nikąd, jeśli ktoś wyznaje takie podejście że "wyjątki są tylko do błędów", to wpada też na pomysły że: funkcje nie mogą być propertisami (bo "funkcje to akcje"), albo mówi że dziedziczenie to "dodanie relacji as-is" (co też nie jest prawdą), albo że whilem nie można iterować po kolekcji, "bo od tego jest for".

Nie ma znaczenia że wyjątki w większości języków nazywają się Exception albo Error - jeśli weźmiesz je na poważnie, to jest to po prostu mechanizm który przerywa wykonanie do jakiegoś punktu wyżej, bardzo podobnie jak return - z tą różnicą że return wchodzi w jeden poziom wyżej, a throw wchodzi n poziomów wyżej.

3

Ludzie, którzy potrzebują zebrać stack trace, aby wyświetlić użytkownikowi komunikat, że wzrost człowieka nie może być ujemny, to po prostu psychopaci.

0

C# here. Co do wyjątków to lubię je stosować w domenie do kontroli reguł biznesowych, a wynika to z tego, że lubię jak obiekty domenowe nie mają dziwnych zależności czy szczegółów implementacyjnych związanych z obsługą i raportowaniem błędów.

Wyjątki biznesowe są łapane w warstwie wyżej (application layer) i tam już tłumaczone na coś w stylu Result<>

Natomiast zgadzam się, że używanie wyjątków do sprawdzania prostych reguł w stylu liczba jest ujemna, albo pole zawiera więcej niż n dopuszczalnych znaków to głupota. Są do tego dedykowane biblioteki które można wpiąć w pipeline obsługi np requestów HTTP i robić taką walidację bezpośrednio na DTO.

2

@markone_dev:

a wynika to z tego, że lubię jak obiekty domenowe nie mają dziwnych zależności czy szczegółów implementacyjnych związanych z obsługą i raportowaniem błędów.

należy czytać jako: nie chce mi się rozpisać jawnie obsługi błędów, więc wolę zrobić goto z jakiegokolwiek miejsca w kodzie :P

0

@WeiXiao:

Od razu lenistwo :P Po prostu nie widzę sensu w zaśmiecaniu modelu dziedzinowego szczegółami implementacyjnymi obsługi błędów. Korzystam z tego co oferuje mi natywnie język/framework. Poza tym czym to się różni od stosowania bool czy jakiegoś enuma ze statusami jako return type?

Jak kiedyś do dotneta wprowadzą coś takiego jak Javovy Either to zrewiduję swoje podejście. Póki co na to się nie zapowiada i stosowanie w dotnecie
czegoś w rodzaju Result Object Pattern wymaga korzystania z zewnętrznych bibliotek albo pisania tego samemu. A ja lubię jak mój model dziedziny ma minimum zależności, a już w szczególności jak nie ma zależności do zewnętrznych bibliotek które mogą nagle stać się nie aktualne.

Z resztą z tego samego powodu jak używam EF jako ORM, to w modelu dziedziny nie stosuję atrybutów (w Javie adnotacji) tylko używam Fluent Configurations i mappingi/konfiguracje mojego modelu dziedziny jak i zależności do ORM-a trzymam poza dziedziną.

2

@markone_dev:

Ewidentnie generyczna klaska z 3 metodami i 3 polami to tak duży tech purity debt, że żaden czysty architekt nie powinien nawet rozważać użycia czegoś takiego :P

0

@WeiXiao:

No dobra, ale co jest złego w tym ukrytym goto zwanym wyjątkami oprócz tego że ktoś powiedział/napisał, że "wyjątki jak nazwa wskazuje służą do obsługi sytuacji wyjątkowych" i dlaczego błąd lub null w parametrze przekazanym do obiektu typu Connection drivera do bazy danych jest sytuacją wyjątkową i uzasadnia rzucenie wyjątku, a nie spełnienie reguły biznesowej już nie jest? Kto ma prawo do decydowania o tym co jest sytuacją wyjątkową w ramach której jest uzasadnione stosowanie wyjątków a co nie? :P

Idąc tym tropem to wszędzie powinniśmy stosować Result Object Pattern zamiast smutnych wyjątków :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1