Czyli tak jak myślałem — „masz problem z wyskoczeniem z pętli, to nie używaj pętli”. Brawo.
Chyba nie umiesz czytać ze zrozumieniem - używaj pętli wtedy kiedy trzeba. Używanie pętli nie jest równoznaczne z pisaniem spaghetti. Możesz oczywiście nawalić 10-krotnie zagnieżdżonych pętli i drabinek ifów i potem mieć problem z ogarnięciem tego, ale to znaczy że albo zrobiłeś kupę bo nie umiesz inaczej, albo używasz języka, który jest na tyle ograniczony że się nie da inaczej, albo masz tak specyficzny problem że faktycznie inaczej się nie da.
Z doświadczenia wiem, że w programowaniu wysokopoziomowym ostatni przypadek występuje niezmiernie rzadko, a najczęściej ten pierwszy.
No i używam pętli wtedy kiedy trzeba. Tak samo używam pętli zagnieżdżonych wtedy, kiedy potrzeba, a nie kiedy Księżyc jest w nowiu. Kiedy muszę przeskanować każdą komórkę wielowymiarowej macierzy (np. trójwymiarowej), to używam wielu pętli i zagnieżdżeń. I kiedy muszę wyskoczyć z wnętrzna tych pętli na zewnątrz, to elegancko robię goto
i sobie wyskakuję.
Kupą jest twoja argumentacja i nazywanie wszystkiego spaghetti, a nie prosty, czytelny i efektywny goto
.
Są języki które mają labeled break. Np Java i Rust mają, więc nie musisz używać goto aby wyskoczyć z zagnieżdżonej pętli tylko piszesz break outer
albo break 'outer
.
Natomiast w niektórych językach nie ma takiej konstrukcji (np. C) i wtedy goto jest jak najbardziej uzasadnione.
Podobnie skok w przód do kodu czyszczenia zasobów, w przypadku wystąpienia błędu. Nie wszystkie języki mają RAII lub wyjątki.
BTW wyjątki są pod pewnym względem znacznie gorsze niż goto - bo nie widać miejsca z którego mogą być rzucone. Goto jest jawne, widzisz słowo kluczowe goto i wiesz że w tym miejscu sterowanie przeniesie się w inne miejsce. Natomiast wywołanie metody rzucającej wyjątek wygląda identycznie jak wywołanie każdej innej metody. Czyli jak masz 10 wywołań jedno po drugim to tak jakbyś miał 10 ifów z goto, tyle że niewidocznych. I dziwnym trafem nikt nie wiesza na wyjątkach psów aż tak jak na goto. Błędy w obsłudze błędów, np. pomijanie czyszczenia zasobów w sytuacji wyjątkowej wykrywam regularnie na CR kodu w Javie.
Podsumowując - dla mnie goto to tylko jeden z wielu mechanizmów. Można to użyć właściwie albo niewłaściwie w konkretnym języku, ale samo w sobie nie jest złe ani dobre.
furious programming napisał(a):
Jeśli ów błąd nie generuje wyjątku lub wyjątki mogą być wyłączone, to
goto
ma zastosowanie w dowolnym języku.
Nie specjalnie widzę do czego. Masz błąd bez wyjątku, to dalej masz najczęściej RAII, defer
lub GC we współczesnych językach (do wyboru). Więc nie musisz się przejmować goto
.
Może rozwiniesz trochę swoją wypowiedź i napiszesz coś więcej na temat tych „nowoczesnych języków”? Załóżmy, że chodzi o C# czy Javę (czy jakikolwiek inny język miałeś na myśli) i o kod podobny do tego z przytoczonego artykułu, czyli próba wykonania kilku operacji i jeśli coś pójdzie nie tak, to sprzątamy zasoby. Albo o skoki pomiędzy
case
ami i innymi blokami, czyli o reużywanie bloków kodu, bez jawnego ich wydzielania do osobnych funkcji (z jakiegokolwiek powodu). Albo o wyskakiwanie z wielokrotnie zagnieżdżonych pętli.
Sprzątanie zasobów
fn createFoo(param: i32) !Foo {
const foo = try tryToAllocateFoo();
// now we have allocated foo. we need to free it if the function fails.
// but we want to return it if the function succeeds.
errdefer deallocateFoo(foo);
const tmp_buf = allocateTmpBuffer() orelse return error.OutOfMemory;
// tmp_buf is truly a temporary resource, and we for sure want to clean it up
// before this block leaves scope
defer deallocateTmpBuffer(tmp_buf);
if (param > 1337) return error.InvalidParam;
// here the errdefer will not run since we're returning success from the function.
// but the defer will run!
return foo;
}
Rust:
fn create_foo(param: i32) -> Result<Box<Foo>, ()> {
let foo = Box::new(foo);
let mut buf = vec![];
buf.try_reserve(1234).map_err(|_| ())?;
if param <= 1337 { Ok(foo) } else { Err(()) }
}
Go:
func createFoo(param i32) (*Foo, error) {
foo := new(Foo)
buf := make([]int, 1234) // Tutaj nie za bardzo mamy opcję by obsłużyć błąd, bo mamy GC
if param <= 1337 { return nil, someError }
return foo, nil
}
Przeskakiwanie między blokami bez wydzielania współdzielonego kodu do osobnych funkcji
Po prostu wydziel kod do osobnych funkcji, po to je bozia dała.
Wyskakiwanie z wielokrotnie zagnieżdżonych pętli
Opcji mamy kilka:
- osobna funkcja w której mamy
return
- często zdecydowanie czytelniejsze niż ileś zagnieżdżonych pętli - iterator chaining/comprehensions/etc. ogólnie podejście bardziej funkcyjne i "przerwanie" pętli tak szybko jak się da
- nazwane pętle, IMHO zdecydowanie czytelniejsze rozwiązanie niż
goto
Przykład z nazwanymi pętlami:
'a: for a in as {
'b: for b in bs {
'c: for c in cs {
match c {
Action::BreakA => break 'a,
Action::BreakB => break 'b,
Action::BreakC => break 'c,
}
}
}
}
Zig:
const std = @import("std");
const expect = std.testing.expect;
test "nested break" {
var count: usize = 0;
outer: for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
count += 1;
break :outer;
}
}
try expect(count == 1);
}
test "nested continue" {
var count: usize = 0;
outer: for ([_]i32{ 1, 2, 3, 4, 5, 6, 7, 8 }) |_| {
for ([_]i32{ 1, 2, 3, 4, 5 }) |_| {
count += 1;
continue :outer;
}
}
try expect(count == 8);
}
Więc jak widać, też się da bez wprowadzania dodatkowej konstrukcji, która zaciemnia przepływ kodu. Więc tak, jak najbardziej goto
ma sens w C, ale w innych językach - no już zdecydowanie, zdecydowanie mniej (o ile w ogóle).
A ja powiem, że rozumiem dlaczego @furious programming pisze to co pisze. Po prostu pewnie nigdy nie używał i nie zna języków nowszych od C i Pascala i nie wie jakie mogą być inne możliwości niż goto
i dlaczego w nowszych językach o bardziej nowoczesnej składni goto
nie ma już sensu.
Pisałem sporo C, baza kodu była nie mniejsza niż kernel Linuksa. Goto było używane do obsługi błedów i dealokacji zespobów na cebulkę. Tyle, że zalecenie było aby korzystać z goto tylko na krytycznych wydajnościowo ścieżkach. Moim zdaniem goto było używane tylko dlatego, że ten język jest już stary i nie ma lepszego mechanizmu.
Korzystanie z goto w języku wysokiego poziomu, to świadome sprowadzanie go do poziomu asemblera. Kompletnie bez sensu.
Krolik napisał(a):
Są języki które mają labeled break. Np Java i Rust mają, więc nie musisz używać goto aby wyskoczyć z zagnieżdżonej pętli tylko piszesz
break outer
albobreak 'outer
.Natomiast w niektórych językach nie ma takiej konstrukcji (np. C) i wtedy goto jest jak najbardziej uzasadnione.
Podobnie skok w przód do kodu czyszczenia zasobów, w przypadku wystąpienia błędu. Nie wszystkie języki mają RAII lub wyjątki.
BTW wyjątki są pod pewnym względem znacznie gorsze niż goto - bo nie widać miejsca z którego mogą być rzucone. Goto jest jawne, widzisz słowo kluczowe goto i wiesz że w tym miejscu sterowanie przeniesie się w inne miejsce. Natomiast wywołanie metody rzucającej wyjątek wygląda identycznie jak wywołanie każdej innej metody. Czyli jak masz 10 wywołań jedno po drugim to tak jakbyś miał 10 ifów z goto, tyle że niewidocznych. I dziwnym trafem nikt nie wiesza na wyjątkach psów aż tak jak na goto. Błędy w obsłudze błędów, np. pomijanie czyszczenia zasobów w sytuacji wyjątkowej wykrywam regularnie na CR kodu w Javie.
Podsumowując - dla mnie goto to tylko jeden z wielu mechanizmów. Można to użyć właściwie albo niewłaściwie w konkretnym języku, ale samo w sobie nie jest złe ani dobre.
Wszystko się sprowadza do tego czy używasz goto
żeby wrócić do tego samego scope'u (albo nadrzędnego lub podrzędnego), i wtedy jego użycie nie ma specjalnych wad - to po prostu instrukcja sterująca przepływem i działa spoko; czy używasz go żeby skakać między scope'ami - i wtedy to już jest mega słabe i prowadzi do samych nieszczęść. Jeśli masz na tyle dyscypliny żeby nie skoknąć do złego scope'a takim goto
, to moim zdaniem możesz go używać. Jeśli nie masz, i potrzebujesz pomocy kompilatora który Cię przypilnuje (i pozwoli na skoki jedynie continue
, break
, for
, throw
) to wtedy lepiej go nie używać.
To powiedziawszy - jestem fanem programowania strukturalnego zapoczątkowanego przez Dijkstre i "goto considered harmful" i tak samo jak przedmówcy jestem zdania że aplikacje są dużo czytelniejsze jak używa się standardowych elementów kontroli przepyłwu i goto
nie jest niezbedne; a wręcz (moim zdaniem) lepiej jakby go nie było.
A tak w ogóle, jak czytam te wypowiedzi nt goto tutaj, to przeprowadzam taką metaanalizę w głowie; i wydaje mi się że wiem czemu niektórzy mają chęć do używania goto, zwłaszcza w przykładzie z góry do skakania do różnych case
ów w switchu - To chyba jest chęć aspirowania do paradygmatu deklaratywnego, nie chcemy imperatywnie wołać funkcji, tylko chcemy powiedzieć "niech teraz program będzie w tej innej akcji ze switcha"; tylko że tak jak w programowaniu deklartywnym to jest dobrze oprogramowanie, tak takim goto
można sobie łatwo strzelić w stopę.
No w sumie named loop można rozumieć jako takie lokalne goto
tylko do pętli, większość nowych języków to ma, a goto
jako takiego już nie.
w wiekszości nowszych języków nie ma goto . Najczęsciej zadawnym pytaniem odnośnie tych jezyków jest "jak zrobic odpowiednik goto" ;)
gajusz800 napisał(a):
No w sumie named loop można rozumieć jako takie lokalne
goto
tylko do pętli, większość nowych języków to ma, agoto
jako takiego już nie.
Tylko że z named loop nie wyskoczysz do innego scope'u - możesz wrócić do parenta, czyli możesz zrobić to samo co wyjątkiem albo returnem - i to jest w porządku.
Gdyby kompilatory miały wbudowane checki, które pozwalają używać goto
, ale w taki sposób że nigdy nie wyszłyby poza scope parenta, wtedy byłby całkowicie bezpieczne i możnaby ich użyć - ale niestety nie ma czegoś takiego, a więc i samo goto
jest potencjalnie niebezpieczne.
Miang napisał(a):
Najczęsciej zadawnym pytaniem odnośnie tych jezyków jest "jak zrobic odpowiednik goto" ;)
Nie wiem gdzie, chyba w gimnazjum.
Riddle napisał(a):
Tylko że z named loop nie wyskoczysz do innego scope'u - możesz wrócić do parenta, czyli możesz zrobić to samo co wyjątkiem albo returnem - i to jest w porządku.
No dlatego napisałem, że to takie lokalne goto, nie możesz skakać gdzie chcesz, a tylko przerwać pętlę.
gajusz800 napisał(a):
Riddle napisał(a):
Tylko że z named loop nie wyskoczysz do innego scope'u - możesz wrócić do parenta, czyli możesz zrobić to samo co wyjątkiem albo returnem - i to jest w porządku.
No dlatego napisałem, że to takie lokalne goto, nie możesz skakać gdzie chcesz, a tylko przerwać pętlę.
Nie wprowadzaj takich mieszanych dziwnych terminów. To co chciałes powiedzieć, to to że to jest lokalna instrukcja kontroli przepływu; a to nie to samo. Wyrażaj się precyzyjniej.
Riddle napisał(a):
Tylko że z named loop nie wyskoczysz do innego scope'u - możesz wrócić do parenta, czyli możesz zrobić to samo co wyjątkiem albo returnem - i to jest w porządku.
wyjątek nie służy do wracania do parenta. wyjątek ma rację bytu w sytuacji wyjątkowej a nie do sterowania przepływem pętli
Miang napisał(a):
Najczęsciej zadawnym pytaniem odnośnie tych jezyków jest "jak zrobic odpowiednik goto" ;)
Nie wiem gdzie, chyba w gimnazjum.
there is no gimnazjum any more
Jest jeszcze jedno zastosowanie goto, którego nie da się zastąpić niczym innym w tych wysokopoziomowych językach - co z computed goto?
Czyli cel skoku jest obliczony np. wzięty z tablicy, a nie wbity na stałe.
Tak wiem, zastosowanie bardzo niszowe, głównie w interpreterach / FSM, ale może mieć potencjalnie znaczną przewagę wydajnościową nad wskaźnikami do funkcji (w przypadku wskaźników, funkcja najpierw musi wrócić, potem dopiero następuje skok do kolejnej; ponadto źródło skoku jest wtedy jedno a punktów docelowych wiele co nieco utrudnia działanie jednostki przewidywania skoków w CPU).
Nie wiem skąd przekonanie, że wydzielenie kodu do osobnych funkcji, w ramach reużywania danego bloku, jest tożsame z goto
. No nie, nie jest, bo wywołanie funkcji:
- jest znacznie bardziej kosztowne (tworzenie ramki stosu, wypełnianie parametrami, skoki, powroty) — można próbować
inline
ować, ale kompilator może to zignorować (bo to tylko sugestia), - odbiera się dostęp do lokalnych zmiennych — trzeba je globalizować lub pchać w parametrach,
- wymusza się duplikację kodu — w prostych kodach może i nie jest to duży problem, ale przy bardziej złożonym przepływie sterowania zrobi się nieczytelne spaghetti, trudniejsze do analizy i debugowania,
- obniża się czytelność — kod rozbity na wiele funkcji nie jest tak prosty do analizy jak jeden blok z widocznymi skokami.
- nie rozwiązuje problemu dowolnej reużywalności bloku — alternatywą jest zapętlenie wywołań lub rekurencja, czyli z deszczu pod rynnę.
Mamy np. funkcję, w której ciele znajduje się kilka(naście/dziesiąt) małych bloków, coś jak to co podano w tym poście, ale niekoniecznie w formie switch
a. Przepływ sterowania wygląda tak, że te małe bloki wykonywane są jeden po drugim — w niektórych przypadkach nastepuje skok do kolejnego, a niektórych do któregoś dalszego, w niektórych do któregoś poprzedniego (coby powtórzyć dany fragment), a w niektórych jest return
. W takim przypadku użycie goto
jest najlepszym co można zrobić, bo daje możliwość skakania po blokach w dowolny sposób — wykonywać kolejne bloki, niektóre pomijać, niektóre powtarzać (jeśli jest taka konieczność). Rozbicie tego na funkcje stworzy turbo-spaghetti.
W podlinkowanym artykule są przykłady, które pokazują użyteczność goto
w C, np. funkcja, na końcu której zawsze zwalnia się zasoby. Wiadomo, że w przypadku wielu wysokopoziomowych języków jest jakaś forma sprzątaczki, tak w poprzednim poście wspomniałem nie o zwalnianiu zasobów, a o jakiejkolwiek finalizacji. Taką finalizacją może to być zwolnianie zasobów, ale może to też być cokolwiek innego, np. wywołanie kilku funkcji, modyfikacja parametrów wyjściowych itd. Do tego idealnie nadaje się goto
(jak pokazano w artykule) i jednocześnie nie widzę cukru, który by go zastąpił — jeśli znacie, podajcie proszę.
Praktycznymi przykładami czegoś w ten deseń są funkcje szyfrujące, w których skoki mogą nastepować do przodu lub do tyłu (reużywanie konkretnych kawałków wykonujących obliczenia) i w których jednak wydajność jest ważna. Ale zastosowań praktycznych jest znacznie, znacznie więcej.
Podano też przykład named loop
ów — fajny cukier, pozwala skakać po pętlach i z nich dowolnie wyskakiwać, praktyczny ficzer. Nie jest to jednak zamiennik goto
, bo jest ograniczony wyłącznie do pętli i powtarzania/przerywania iteracji. W językach, w których nie ma wsparcia nazwanych pętli (np. w C czy Pascalach), nadal można je zrobić za pomocą poczciwego goto
:
label
LoopA, LoopB, LoopC, LoopOut;
var
A, B, C: Integer;
begin
for A in AA do begin
for B in BB do begin
for C in CC do begin
if {condition} then goto LoopA;
if {condition} then goto LoopB;
if {condition} then goto LoopC;
if {condition} then goto LoopOut;
LoopC: end;
LoopB: end;
LoopA: end;
LoopOut:
// more code
end;
W Pascalach wygląda to słabo, bo składnia rozwlekła, w dodatku etykiety muszą być deklarowane jak zmienne — w C wyglądałoby to znacznie lepiej. Minus jest taki, że skoki muszą być wykonywane na koniec bloku pętli — przeskoczenie do nagłówka spowoduje pominięcie inkrementacji licznika tej pętli (nastąpi powtórzona iteracja). Choć jeśli zależy nam na tym, aby skok nie inkrementował licznika, to jak najbardziej mamy taką możliwość. No i zawsze mamy możliwość skoczenia do dowolnego miejsca dowolnej pętli, a nie tylko wykonania kolejnej iteracji, więc mamy pełną kontrolę i pełną swobodę nad przepływem sterowania.
Oprócz zwykłych goto
, działających w ramach konkretnego, głównego bloku kodu (np. w obrębie ciała funkcji), są jeszcze non-local goto
. To jest dopiero ciekawy ficzer, bo pozwala na skoki np. pomiędzy funkcjami. Niestety nie znam jego praktycznych zastosowań, nigdy z niego nie korzystałem, więc nie podzielę się przykładem.
Zgadzam się z @furious programming, szczególnie z tym, że używając goto
można uniknąć dwukrotnego wykonywania kodu. Na przykład chcemy pobrać od użytkownika liczbę z zakresu od 1 do 10, w razie podania liczby spoza zakresu wypisać odpowiedni komunikat i poprosić o podanie liczby powtórnie. Oto rozwiązanie bez instrukcji skoku.
do
{ printf ("Liczba? ");
scanf ("%d", &liczba);
if (liczba < 1 || liczba > 10)
puts ("Liczba poza zakresem 1-10.");
} while (liczba < 1 || liczba > 10);
Trzeba 2 razy wyklepać warunek liczba < 1 || liczba > 10
, tyle samo razy musi go sprawdzić procesor. Dzięki skokowi można połączyć decyzję o wyświetleniu komunikatu z powtórzeniem pętli.
popraw:
printf ("Liczba? ");
scanf ("%d", &liczba);
if (liczba < 1 || liczba > 10)
{ puts ("Liczba poza zakresem 1-10.");
goto popraw;
}
Język C zawiera goto, a język C jest uważany za najlepszy do zadań niskiego poziomu, jeżeli goto by było złe to język C by nie umożliwiał stosowanie instrukcji goto.
A jeżeli chodzi o to, czy żaden nowy język nie zawiera goto zawiera PHP zawiera.
Przecież jądro linuxa ma dużo instrukcji goto.
tomixtomi0001 napisał(a):
Język C zawiera goto, a język C jest uważany za najlepszy do zadań niskiego poziomu, jeżeli goto by było złe to
język C by nie umożliwiał stosowanie instrukcji goto.
język C pozwala na wiele głupich rzeczy.
Wszystkie instrukcje przerwań to zło tak samo jak mutowalność by default - to tylko zaciemnia czytanie kodu.
Jak dla mnie nie ma znaczenia czy chodzi o goto czy o break, continue, return, throw. Wszystkie te instrukcje powodują, że muszę patrzeć na znacznie szerszy kontekst niż mógłbym, takiego kodu się czyta lecz bada z debbugerem, skacząc z jednego miejsca w drugie.
Warto zauważyć, że im bardziej język jest ukierunkowany na tworzenie wyrażeń i lewniwe wartościowanie tym rzadziej potrzebne są przerwania. Wyrażenia jako zapis ukrywają wiele instrukcji i dzięki nim kod jest prostszy do zrozumienia niż mechaniczny odpowiednik z przerwaniami.
znowutosamo napisał(a):
Wyrażenia jako zapis ukrywają wiele instrukcji i dzięki nim kod jest prostszy do zrozumienia niż mechaniczny odpowiednik z przerwaniami.
Przecież to samozaprzeczenie — jak coś może być prostsze do zrozumienia, skoro istota została ukryta i stała się niejawna? To właśnie jawne skoki pozwalają uniknąć kontrowersji, natomiast ukrywanie przepływu sterowania za cukrem, przeładowanymi operatorami czy kupką funkcji zmusza do analizy wielu miejsc i patrzenia na szerszy kontekst.
Przy okazji — goto
nie jest przerwaniem, a skokiem bezwarunkowym.
Jawne skoki utrudniają czytanie kodu, bo trzeba śledzić gdzie są wykonane. Nie wiem czego tu nie rozumiesz? Im więcej skoków w różne miejsca, tym kod mniej czytelny i bardziej zagmatwany.
Ach, zapomniałem, że tych niejawnych śledzić nie trzeba. :D
Jeśli w kodzie używasz GOTO (pomijam asemblery), to na 99% robisz coś źle. Albo Twoje funkcje/metody są zbyt duże i wykonują wiele operacji (sprzeczność z SRP), albo źle posługujesz się dostępnymi narzędziami.
Są w prawdzie pewne bardzo nieliczne problemy, gdzie goto jest jedyną sensowną opcją. Ale to są naprawdę bardzo rzadkie przypadki i na co dzień nie spotyka się ich.
Jeśli chodzi o C - też są dużo lepsze metody niż używanie goto
C++ - nawet już nie wspomnę (chociaż przez kilka lat pracowałem przy projekcie, w którym kluczowa funkcja miała ponad 2000 linii kodu i niezliczone goto w środku)
Nowsze języki - no to nawet nie ma co pisać.
GOTO (instrukcja skoku (bez)warunkowego) sprawdza się tylko i wyłącznie w asemblerach i językach bardzo niskiego poziomu, gdzie po prostu nie ma innej opcji.
Juhas napisał(a):
Jeśli chodzi o C - też są dużo lepsze metody niż używanie goto
Napisz jakie.
GOTO (instrukcja skoku (bez)warunkowego) sprawdza się tylko i wyłącznie w asemblerach i językach bardzo niskiego poziomu, gdzie po prostu nie ma innej opcji.
No jak widać w podlinkowanym wcześniej artykule — https://blog.joren.ga/gotophobia-harmful — przypadków, w których goto
jest świetną rzeczą (wręcz najlepszym rozwiązaniem) jest trochę. Poza tym ten artykuł zawiera tylko niektóre przypadki.
Dobra, to zróbmy inaczej, bo na razie przez kilka stron tego wątku odstawiane jest filozofowanie, bez jakichkolwiek sensownych przykładów. Robimy tak, bierzemy każdy przykład z podlinkowanego artykułu — GOTOphobia considered harmful — i podajemy kontrprzykład z dowolnych innych języków ogólnego przeznaczenia, najlepiej z tych, które istnieją na rynku od dłuższego czasu i które są faktycznie najpopularniejsze na świecie — C, C++, C#, Go, Rust, Python, Java, JavaScript, PHP itd.
Przy okazji @Juhas stwierdził, że goto
jest zbędne w każdym języku innym niż Assembly (że są „dużo lepsze metody niż używanie goto
”), więc może być tym, który poda rozwiązania w języku C, lepsze niż te z goto
podane w podlinkowanym artykule.
To będzie sensowne, merytoryczne podejście do poruszanego problemu. Nie tylko rozwieje wszelkie wątpliwości, ale też pozwoli porównać różne języki i ich funkcjonalność, co będzie znacznie bardziej wartościowe niż rzucanie na lewo i prawo pustych stwierdzeń. Podejdźcie do tego jak do zwykłego FizzBuzzu, pokażcie jakimi ficzerami dysponujecie w językach, z którymi pracujecie.
PS: pierwszego problemu z tego artykułu — Error/exception handling & cleanup — nie rozpatrujcie wyłącznie pod kątem zwalniania zasobów, bo są w nim funkcje clean_stuff
oraz undo_something
, które nie dotyczą stricte zwalniania zasobów, a jakiejkolwiek finalizacji oraz cofania zmian wprowadzonych przed wykryciem problemu/błędu. Tak więc istnienie RAII/GC nie powoduje, że problem cleaupu/finalizacji wyparowuje.
Nie rozumiem dlaczego nested if jest zabroniony skoro i tak jest czytelniejszy do goto ale to jest moja luźna propozycja w Pythonie do dyskusji:
def clean_stuff():
if cleaner.need_clear is True:
clean_stuff_orginal()
def destroy_stuff():
if cleaner.need_clear is True:
destroy_stuff_orginal()
def undo_something():
if cleaner.need_clear is True:
undo_something_orginal()
def foo(bar: int):
return_value = None
if do_something(bar, cleaner) and init_stuff(bar, cleaner) and prepare_stuff(bar, cleaner):
return_value = do_the_thing(bar)
clean_stuff(cleaner)
destroy_stuff(cleaner)
undo_something(cleaner)
return return_value
import contextlib
# gdy funkcje zwracają obiekt reprezntujacy zasób, jaki zwalniamy metodą .close
def foo(bar):
with do_something(bar), init_stuff(bar), prepare_stuff(bar):
return do_the_thing(bar)
# wersja 2, gdy funkcje nie zwracają obiektów
def closable(init, close):
@contextlib.contextmanager
def context():
try:
yield init()
finally:
close()
return context()
def foo(bar):
with closable(lambda: do_something(bar), undo_something),
closable(lambda: init_stuff(bar), destroy_stuff),
closable(lambda: prepare_stuff(bar), clean_stuff):
return do_the_thing(bar)
EDIT:
def required(x):
if not x:
raise Exception()
def foo(bar):
with closable(lambda: required(do_something(bar)), undo_something),
closable(lambda: required(init_stuff(bar)), destroy_stuff),
closable(lambda: required(prepare_stuff(bar)), clean_stuff):
return do_the_thing(bar)
# Wersja 3
def foo(bar):
try:
required(do_something(bar))
try:
required(init_stuff(bar))
try:
required(prepare_stuff(bar))
return do_the_thing(bar)
finally:
clean_stuff()
finally:
destroy_stuff()
finally:
undo_something()
Scala 3:
class IfResource(success: Boolean, onClose : => Unit) extends AutoCloseable with (() => Boolean) :
def apply() = success
override def close() = onClose
def foo(bar: Int) : Option[Int] =
Using.resource(IfResource(do_something(bar), undo_something())) { proceed =>
if proceed() then
Using.resource(IfResource(init_stuff(bar), destroy_stuff())) { init =>
if init() then
Using.resource(IfResource(prepare_stuff(bar), cleanup_stuff())) { prepare =>
if prepare() then
Some(do_the_stuff(bar))
else None
} else None
} else None
}
Normalnie nikt by tak nie napisał (drabinka ifów) tylko pewnie użył monady (i np. ZIO.scoped + for comprehension), ale chciałem zobaczyć sam wersję bez bibliotek. To powyżej pierwsze co przyszło mi do głowy.
EDIT:
przyszło mi do głowy jak to zrobić bez bibliotek typu ZIO:
type ToClose = Seq[()=> Unit]
def closeAfter
(impureOperation: => Boolean, prevClose : ToClose = Seq.empty )
(onClose: () => Unit): Either[ToClose, (Boolean, ToClose)] =
val result = impureOperation
if result then
Right((result, onClose +: prevClose))
else
Left(onClose +: prevClose)
def foo(bar: Int) : Option[Int] = {
val initialization = for
(_, clean1) <- closeAfter(do_something(bar))(undo_something)
(_, clean2) <- closeAfter(init_stuff(bar), clean1)(destroy_stuff)
(ok, clean3) <- closeAfter(prepare_stuff(bar), clean2)(cleanup_stuff)
yield (ok, clean3)
val result = initialization.map {
case (ok, toClean) =>
if ok then
(Some(do_the_stuff(bar)), toClean)
else
(None, toClean)
}.left.map( (None, _))
.merge
result._2.foreach( _())
result._1
}
zaletą niby jest to, że nie ma drabinki ifów, ale konieczność pamiętania w for o przekazaniu argumentów clean1, clean2 jest słaba, dlatego właśnie biblioteki się jednak przydają czasem.
EDIT2:
Wersja ZIO, bo inne "nieczytelne"
import zio._
def cleanAfter(proceed: => Boolean, toClose: => Unit) =
ZIO.fromAutoCloseable(ZIO.succeed(new AutoCloseable {
override def close(): Unit = toClose
})).flatMap(_ => if proceed then ZIO.succeed(proceed) else ZIO.fail(()))
def foo(bar: Int) =
ZIO.scoped {
for
_ <- cleanAfter(do_something(bar), undo_something())
_ <- cleanAfter(init_stuff(bar), destroy_stuff())
ok <- cleanAfter(prepare_stuff(bar), cleanup_stuff())
yield (if ok then Some(do_the_stuff(bar)) else None)
}
@furious programming: zacznijmy może np. z idiomatycznym Rustem:
// Musimy zwrócić `Box` lub `&'static` bo inaczej nie mamy czego zwrócić, bo lifetime
fn foo(bar: i32) -> Result<Box<i32>, Error> {
let _something = do_something(bar)?;
let _stuff = init_stuff(bar)?;
let _preparation = prepare_stuff(bar)?;
do_the_thing(bar)
}
Ale pewnie będziesz miał problem z tym, że nie wywołuję tutaj funkcji "z palca" bo przecież te funkcje mogą robić "dowolne rzeczy" jak opisałeś (mimo iż ich nazwy oczywiście wskazują na czyszczenie po poprzednich wywołaniach). A mimo wszystko piszesz o:
które nie dotyczą stricte zwalniania zasobów, a jakiejkolwiek finalizacji oraz cofania zmian wprowadzonych przed wykryciem problemu/błędu
Czyli dokładnie do tego do czego służy RAII. Przecież RAII to nie tylko zwalnianie zasobów, ale jakakolwiek finalizacja. Przykład znów z Rusta MutexGuard
czyli struktura RAII, której całym raison d'etre jest to, by zamknąć mutex bez ingerencji użytkownika (nic to nie mówi o zwalnianiu jakichkolwiek zasobów).
Tak więc jeśli ww. funkcje do_something
, init_stuff
i prepare_stuff
są idiomatycznie napisane i zwracają sensowne typy (jak ww. MutexGuard
) to mamy wszystko ładnie zwolnione/wyczyszczone/cofnięte/whatever bez ingerencji programisty. Żadne goto
czy inny mechanizm nie jest tutaj potrzebny, bo do_something(u32) -> Result<Guard, Error>
ładnie nam się tym zajmie, bez konieczności naszej pracy. Jeśli chcesz "manualnie" wywołać czyszczenie to można napisać własny GenGuard
, który to zrobi. Chociaż szerze przykład z foo
jaki zalinkowałeś jest błędny IMHO, bo powinien wyglądać jak już tak:
int* foo(int bar)
{
int* return_value = NULL;
if (!do_something(bar)) {
goto error_didnt_sth;
}
if (!init_stuff(bar)) {
goto error_bad_init;
}
if (!prepare_stuff(bar)) {
goto error_bad_prep;
}
return_value = do_the_thing(bar);
clean_stuff();
error_bad_prep:
destroy_stuff();
error_bad_init:
undo_something();
error_didnt_sth:
return return_value;
}
Bo w końcu raczej nie musimy czyścić po czymś co się nie udało (bo skoro się nie udało, to samo powinno po sobie wyczyścić, a nie wymagać tego od programisty). Więc nawet Twój przykład pokazuje, że używanie goto
nie dość, że jest trochę dziwne jak tylko mamy inne systemy obsługi takich spraw, to jeszcze pokazuje, że użycie goto
jest dość trudne by to zrobić poprawnie.