Wątek przeniesiony 2023-03-02 11:00 z Off-Topic przez Riddle.

Instrukcja goto

1

Czyli tak jak myślałem — „masz problem z wyskoczeniem z pętli, to nie używaj pętli”. Brawo.

0

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.

3

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.

4

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.

2
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 caseami 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).

0

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.

0

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.

1
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 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.

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ę.

0

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.

1

w wiekszości nowszych języków nie ma goto . Najczęsciej zadawnym pytaniem odnośnie tych jezyków jest "jak zrobic odpowiednik goto" ;)

0
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, a goto 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.

0
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ę.

0
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.

1
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

3

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).

1

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ć inlineować, 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 switcha. 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.

1

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;
        }
1

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.

4
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

8BE2D12D-2731-436E-A37E-B1DD279AA9FD.jpeg

język C by nie umożliwiał stosowanie instrukcji goto.

język C pozwala na wiele głupich rzeczy.

0

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.

1
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.

0

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.

2

Ach, zapomniałem, że tych niejawnych śledzić nie trzeba. :D

0

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.

2
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.

1

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.

2

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
0
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()
2

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)
    }
2

@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.

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