Dlaczego Lua nie ma stwierdzenia „kontynuuj”?

144

W ciągu ostatnich kilku miesięcy dużo miałem do czynienia z Luą i bardzo lubię większość funkcji, ale wciąż czegoś mi brakuje:

  • Dlaczego nie ma continue?
  • Jakie są obejścia tego problemu?
Dant
źródło
12
Odkąd zadano to pytanie, Lua otrzymała gotooświadczenie, które można wykorzystać do wdrożenia kontynuacji. Zobacz odpowiedzi poniżej.
lhf

Odpowiedzi:

71

W Lua 5.2 najlepszym obejściem jest użycie goto:

-- prints odd numbers in [|1,10|]
for i=1,10 do
  if i % 2 == 0 then goto continue end
  print(i)
  ::continue::
end

Jest to obsługiwane w LuaJIT od wersji 2.0.1

Catwell
źródło
47
Mam nadzieję, że obejmują one continuejeden dzień. gotoWymiana nie wygląda bardzo ładnie i potrzebuje więcej linii. Ponadto, czy nie spowodowałoby to problemów, gdybyś miał więcej niż jedną pętlę wykonującą to w jednej funkcji, obie z ::continue::? Tworzenie nazwy dla każdej pętli nie brzmi jak przyzwoita rzecz.
ET,
66

Sposób, w jaki język zarządza zakresem leksykalnym, stwarza problemy z włączaniem zarówno gotoi continue. Na przykład,

local a=0
repeat 
    if f() then
        a=1 --change outer a
    end
    local a=f() -- inner a
until a==0 -- test inner a

Deklaracja local awewnątrz ciała pętli maskuje zewnętrzną zmienną o nazwie a, a zakres tego lokalnego rozciąga się na warunek untilinstrukcji, więc warunek testuje najbardziej wewnętrzną a.

Gdyby continueistniał, musiałby być ograniczony semantycznie, aby był ważny tylko wtedy, gdy wszystkie zmienne użyte w warunku wejdą w zakres. Jest to trudny warunek do udokumentowania użytkownikowi i wprowadzenia go w kompilatorze. Omówiono różne propozycje dotyczące tego problemu, w tym prostą odpowiedź polegającą na blokowaniu continueza pomocą repeat ... untilstylu pętli. Jak dotąd żaden nie miał wystarczająco przekonującego przypadku użycia, aby włączyć je do języka.

Obejście polega na ogół na odwróceniu warunku, który spowodowałby continuewykonanie a, i zebraniu reszty treści pętli pod tym warunkiem. Tak więc następująca pętla

-- not valid Lua 5.1 (or 5.2)
for k,v in pairs(t) do
  if isstring(k) then continue end
  -- do something to t[k] when k is not a string
end

można napisać

-- valid Lua 5.1 (or 5.2)
for k,v in pairs(t) do
  if not isstring(k) then 
    -- do something to t[k] when k is not a string
  end
end

Jest to wystarczająco jasne i zwykle nie jest obciążeniem, chyba że masz serię wyszukanych eliminacji kontrolujących działanie pętli.

RBerteig
źródło
5
W tle w Pythonie jest to myląca odpowiedź, ponieważ każdy zakres już przed uruchomieniem wie, jakie są jego zmienne lokalne. To znaczy spodziewałem się niezwiązanego błędu zmiennej lokalnej w przypadku osiągnięcia until....
ubershmekel
2
Było dużo dyskusji na ten temat w społeczności Lua przed wprowadzeniem gotodo Lua 5.2. Oczywiście gotoma ten sam problem. Ostatecznie zdecydowali, że bez względu na koszty środowiska wykonawczego i / lub generowania kodu, które miałyby przed nimi chronić, warte są korzyści wynikające z posiadania elastycznego rozwiązania, gotoktóre może być używane do emulacji zarówno na continuewielu poziomach, jak i na wielu poziomach break. Aby uzyskać szczegółowe informacje , musisz przeszukać archiwa listy Lua pod kątem odpowiednich wątków. Odkąd wprowadzili goto, oczywiście nie było to nie do pokonania.
RBerteig
72
Nie ma nic „wystarczająco jasnego” w pisaniu kodu bez kontynuacji. Zagnieżdżanie kodu wewnątrz warunku, w którym należało użyć kontynuacji, jest błędem początkującym, a potrzeba napisania takiego brzydkiego kodu nie powinna budzić sympatii. Nie ma absolutnie żadnego usprawiedliwienia.
Glenn Maynard
4
To wyjaśnienie nie ma sensu. localto dyrektywa tylko dla kompilatora - nie ma znaczenia, jakie instrukcje w czasie wykonywania znajdują się pomiędzy locali użycie zmiennych - nie musisz nic zmieniać w kompilatorze, aby zachować takie samo zachowanie zakresu. Tak, może to nie być takie oczywiste i wymagać dodatkowej dokumentacji, ale, aby powtórzyć, wymaga ZERO zmian w kompilatorze. repeat do break end until trueprzykład w mojej odpowiedzi już generuje dokładnie ten sam kod bajtowy, który kompilator użyłby dalej, jedyną różnicą jest to continue, że nie potrzebowałbyś brzydkiej dodatkowej składni, aby go użyć.
Oleg V. Volkov
7
To, że można przetestować zmienną wewnętrzną, mówi o wadliwym projekcie. Warunek jest poza wewnętrznym zakresem i nie powinien mieć dostępu do zmiennych w nim zawartych. Rozważ odpowiednik w C: do{int i=0;}while (i == 0);fail lub w C ++: do int i=0;while (i==0);również nie powiedzie się („nie został zadeklarowany w tym zakresie”). Niestety za późno, żeby to zmienić teraz w Lua.
Pedro Gimeno
47

Możesz dodatkowo owinąć pętlę, repeat until truea następnie użyć do break endwewnątrz, aby uzyskać efekt kontynuacji. Oczywiście będziesz musiał ustawić dodatkowe flagi, jeśli chcesz również naprawdę breakwyjść z pętli.

Spowoduje to zapętlenie 5 razy, za każdym razem drukując 1, 2 i 3.

for idx = 1, 5 do
    repeat
        print(1)
        print(2)
        print(3)
        do break end -- goes to next iteration of for
        print(4)
        print(5)
    until true
end

Ta konstrukcja przekłada się nawet na dosłowny jeden kod operacji JMPw kodzie bajtowym Lua!

$ luac -l continue.lua 

main <continue.lua:0,0> (22 instructions, 88 bytes at 0x23c9530)
0+ params, 6 slots, 0 upvalues, 4 locals, 6 constants, 0 functions
    1   [1] LOADK       0 -1    ; 1
    2   [1] LOADK       1 -2    ; 3
    3   [1] LOADK       2 -1    ; 1
    4   [1] FORPREP     0 16    ; to 21
    5   [3] GETGLOBAL   4 -3    ; print
    6   [3] LOADK       5 -1    ; 1
    7   [3] CALL        4 2 1
    8   [4] GETGLOBAL   4 -3    ; print
    9   [4] LOADK       5 -4    ; 2
    10  [4] CALL        4 2 1
    11  [5] GETGLOBAL   4 -3    ; print
    12  [5] LOADK       5 -2    ; 3
    13  [5] CALL        4 2 1
    14  [6] JMP         6   ; to 21 -- Here it is! If you remove do break end from code, result will only differ by this single line.
    15  [7] GETGLOBAL   4 -3    ; print
    16  [7] LOADK       5 -5    ; 4
    17  [7] CALL        4 2 1
    18  [8] GETGLOBAL   4 -3    ; print
    19  [8] LOADK       5 -6    ; 5
    20  [8] CALL        4 2 1
    21  [1] FORLOOP     0 -17   ; to 5
    22  [10]    RETURN      0 1
Oleg V. Volkov
źródło
4
Ta odpowiedź jest fajna, ale nadal wymaga 3 linii zamiast tylko jednej. (jeśli "kontynuuj" było właściwie obsługiwane) Jest to jednak trochę ładniejsze i bezpieczniejsze niż etykieta goto, ponieważ dla tej nazwy może być konieczne uniknięcie kolizji nazw dla zagnieżdżonych pętli.
ET
3
pozwala jednak uniknąć „prawdziwego” problemu z goto, polegającego na tym, że nie musisz wymyślać nowego identyfikatora / etykiety dla każdego pseudo-kontynuuj i jest mniej podatny na błędy, ponieważ kod jest modyfikowany w czasie. zgadzam się, że kontynuacja byłaby przydatna , ale ta IMO jest następną najlepszą rzeczą (i naprawdę wymaga dwóch wierszy na powtórzenie / do w porównaniu z bardziej formalnym „kontynuuj”; .. a nawet wtedy, jeśli tak bardzo interesuje Cię linia liczy, że zawsze można napisać „powtarzaj” i „do końca”, na przykład: gist.github.com/wilson0x4d/f8410719033d1e0ef771 )
Shaun Wilson
1
Miło widzieć, że ludzie faktycznie rozważają wydajność, a nawet zapewniają luacwyjście na SO! Życzę wszystkim zasłużonych głosów :)
DarkWiiPlayer
17

Prosto od samego projektanta Lua :

Naszym głównym zmartwieniem w przypadku „kontynuuj” jest to, że istnieje kilka innych struktur kontrolnych, które (naszym zdaniem) są mniej więcej tak ważne jak „kontynuuj”, a nawet mogą je zastąpić. (Np. Zerwij z etykietami [jak w Javie] lub nawet bardziej ogólnym poleceniem goto.) „Kontynuuj” nie wydaje się bardziej szczególne niż inne mechanizmy struktury kontrolnej, z wyjątkiem tego, że występuje w większej liczbie języków. (Perl ma właściwie dwie instrukcje „kontynuuj”, „next” i „redo”. Obie są przydatne.)

Stuart P. Bentley
źródło
5
Uwielbiam to przyznanie się: „Oba są przydatne” zaraz po wyjaśnieniu „nie będziemy tego robić”
David Ljung Madison Stellar
2
To było zauważyć, że zakres ich były patrząc na adres gdy zrobił to zrobić, dodając „goto” konstrukt 5,2 (który nie został zwolniony, kiedy ta odpowiedź została napisana). Zobacz tę odpowiedź z 2012 roku , po wydaniu wersji 5.2.0.
Stuart P. Bentley,
3
Zgadza się - ponieważ „goto” jest dobrze rozpoznawalne jako przyzwoita konstrukcja programistyczna. (koniec sarkazmu) No cóż.
David Ljung Madison Stellar
2
Ale nie brzmiało to rozsądniej niż „Po prostu zapomniałem włożyć continuedo Lua, przepraszam”.
neoedmund
17

Pierwsza część odpowiedzi w FAQ jak zabitego zauważył.

Jeśli chodzi o obejście problemu, możesz zawinąć ciało pętli w funkcję i returnwcześniej, np

-- Print the odd numbers from 1 to 99
for a = 1, 99 do
  (function()
    if a % 2 == 0 then
      return
    end
    print(a)
  end)()
end

Lub jeśli chcesz mieć obie breaki continuefunkcjonalność, poproś funkcję lokalną o wykonanie testu, np

local a = 1
while (function()
  if a > 99 then
    return false; -- break
  end
  if a % 2 == 0 then
    return true; -- continue
  end
  print(a)
  return true; -- continue
end)() do
  a = a + 1
end
finnw
źródło
16
Proszę nie. Tworzysz środowisko zamknięcia w każdej iteracji i jest to OGROMNE marnowanie pamięci i cykli GC.
Oleg V. Volkov
4
idź sprawdzić collectgarbage("count")nawet po twoich prostych 100 próbach, a wtedy porozmawiamy. Taka „przedwczesna” optymalizacja uratowała w zeszłym tygodniu jeden projekt o dużym obciążeniu przed ponownym uruchamianiem co minutę.
Oleg V. Volkov
4
@ OlegV.Volkov Chociaż ten przykład powoduje stosunkowo duże obciążenie GC, nie przecieka - wszystkie tymczasowe zamknięcia zostaną zebrane. Nie wiem o twoim projekcie, ale większość powtarzających się ponownych uruchomień IME wynika z wycieków.
finnw
10

Nigdy wcześniej nie używałem Lua, ale wyszukałem go w Google i wymyśliłem to:

http://www.luafaq.org/

Sprawdź pytanie 1.26 .

To częsta skarga. Autorzy Lua uznali, że kontynuacja była tylko jednym z wielu możliwych nowych mechanizmów kontroli przepływu (fakt, że nie może działać z regułami zakresu powtarzania / dopóki nie był czynnikiem drugorzędnym).

W Lua 5.2 znajduje się instrukcja goto, której można łatwo użyć do wykonania tej samej pracy.

zabity
źródło
8

Możemy to osiągnąć jak poniżej, pominie liczby parzyste

local len = 5
for i = 1, len do
    repeat 
        if i%2 == 0 then break end
        print(" i = "..i)
        break
    until true
end

O / P:

i = 1
i = 3
i = 5
Dilip
źródło
6

Napotkaliśmy ten scenariusz wiele razy i po prostu używamy flagi do symulacji kontynuacji. Staramy się również unikać stosowania instrukcji goto.

Przykład: Kod zamierza wydrukować instrukcje od i = 1 do i = 10, z wyjątkiem i = 3. Ponadto wypisuje również „początek pętli”, „koniec pętli”, „jeśli początek” i „jeśli koniec”, aby zasymulować inne zagnieżdżone instrukcje istniejące w kodzie.

size = 10
for i=1, size do
    print("loop start")
    if whatever then
        print("if start")
        if (i == 3) then
            print("i is 3")
            --continue
        end
        print(j)
        print("if end")
    end
    print("loop end")
end

osiąga się poprzez zamknięcie wszystkich pozostałych instrukcji aż do końca zakresu pętli flagą test.

size = 10
for i=1, size do
    print("loop start")
    local continue = false;  -- initialize flag at the start of the loop
    if whatever then
        print("if start")
        if (i == 3) then
            print("i is 3")
            continue = true
        end

        if continue==false then          -- test flag
            print(j)
            print("if end")
        end
    end

    if (continue==false) then            -- test flag
        print("loop end")
    end
end

Nie mówię, że jest to najlepsze podejście, ale dla nas działa doskonale.

winux
źródło
5

Lua to lekki język skryptowy, który powinien być jak najmniejszy. Na przykład wiele operacji jednoargumentowych, takich jak inkrementacja przed / po, nie jest dostępnych

Zamiast kontynuować, możesz użyć goto like

arr = {1,2,3,45,6,7,8}
for key,val in ipairs(arr) do
  if val > 6 then
     goto skip_to_next
  end
     # perform some calculation
  ::skip_to_next::
end
Ankit ihelper Sharma
źródło
4

Ponownie z odwracaniem, możesz po prostu użyć następującego kodu:

for k,v in pairs(t) do
  if not isstring(k) then 
    -- do something to t[k] when k is not a string
end
8lakester
źródło
Problem z inwersją polega na tym, że często w szeregu występuje wiele warunków (takich jak sprawdzanie poprawności danych wejściowych użytkownika). A ponieważ w dowolnym momencie może zaistnieć potrzeba zwarcia, inwersja oznacza konieczność ciągłego zagnieżdżania warunków (zamiast „czy to źle? Potem uciekaj; w przeciwnym razie czy to źle? Potem uciekaj”, co jest bardzo proste, kończysz z kodem typu „czy to w porządku? to czy to w porządku? to czy to w porządku?”, a następnie zrób to „co jest bardzo przesadne.
Leslie Krause
-2

Dlaczego nie ma kontynuacji?

Ponieważ jest to niepotrzebne¹. Jest bardzo niewiele sytuacji, w których deweloper by tego potrzebował.

A) Kiedy masz bardzo prostą pętlę, powiedzmy 1- lub 2-liniową, możesz po prostu odwrócić stan pętli i nadal jest bardzo czytelny.

B) Kiedy piszesz prosty kod proceduralny (czyli sposób, w jaki pisaliśmy kod w ubiegłym wieku), powinieneś również stosować programowanie strukturalne (czyli jak napisaliśmy lepszy kod w ubiegłym wieku)

C) Jeśli piszesz kod zorientowany obiektowo, treść pętli powinna składać się z nie więcej niż jednego lub dwóch wywołań metod, chyba że można to wyrazić w jedno- lub dwuwierszowym (w takim przypadku zobacz A)

D) Jeśli piszesz kod funkcjonalny, po prostu zwróć zwykłe wywołanie końcowe dla następnej iteracji.

Jedynym przypadkiem, w którym chciałbyś użyć continuesłowa kluczowego, jest zakodowanie Lua tak, jak w Pythonie, którym po prostu nie jest .²

Jakie są obejścia tego problemu?

O ile nie ma zastosowania A), w którym to przypadku nie ma potrzeby stosowania żadnych obejść, należy wykonywać programowanie strukturalne, obiektowe lub funkcjonalne. To są paradygmaty, do których stworzono Lua, więc walczyłbyś z językiem, gdybyś zrobił wszystko, co w twojej mocy, aby uniknąć ich wzorców³.


Kilka wyjaśnień:

¹ Lua to bardzo minimalistyczny język. Próbuje mieć tak mało funkcji, jak to tylko możliwe, a continuestwierdzenie nie jest w tym sensie istotną funkcją.

Myślę, że ta filozofia minimalizmu została dobrze ujęta przez Roberto Ierusalimschy w wywiadzie z 2019 roku :

dodaj to i to, i tamto, odłóż to, a na końcu rozumiemy, że ostateczny wniosek nie zadowoli większości ludzi i nie postawimy wszystkich opcji, których wszyscy chcą, więc nic nie wkładamy. W końcu tryb ścisły jest rozsądnym kompromisem.

² Wydaje się, że duża liczba programistów przyjeżdża do Lua z innych języków, ponieważ każdy program, dla którego próbują napisać, używa go, a wielu z nich nie chce pisać niczego innego niż język wybór, co prowadzi do wielu pytań, takich jak „Dlaczego Lua nie ma funkcji X?”

Matz opisał podobną sytuację z Ruby w niedawnym wywiadzie :

Najpopularniejszym pytaniem jest: „Jestem ze społeczności języka X; czy nie możesz wprowadzić funkcji z języka X do Rubiego?” Lub coś w tym rodzaju. A moja zwykła odpowiedź na te prośby brzmi… „nie, nie zrobiłbym tego”, ponieważ mamy inny projekt języka i różne zasady rozwoju języka.

³ Jest kilka sposobów obejścia tego problemu; niektórzy użytkownicy sugerowali użycie goto, co w większości przypadków jest wystarczająco dobrym przybliżeniem, ale bardzo szybko staje się bardzo brzydkie i całkowicie zrywa z zagnieżdżonymi pętlami. Używanie gotos naraża Cię również na ryzyko rzucenia kopii SICP za każdym razem, gdy pokażesz komuś swój kod.

DarkWiiPlayer
źródło
1
Głosowałem w dół, ponieważ pierwsze zdanie jest oczywiście fałszywe, a reszta odpowiedzi jest nieprzydatna.
bfontaine
Nieprzydatny? Może; jest to w pewnym stopniu odpowiedź oparta na opiniach. Jednak pierwsze zdanie jest oczywiście prawdziwe; continuemoże być wygodną funkcją, ale to nie oznacza, że ​​jest to konieczne . Wiele osób używa Lua bez niego, więc naprawdę nie ma powodu, aby był to coś innego niż zgrabna funkcja, która nie jest niezbędna w żadnym języku programowania.
DarkWiiPlayer
To nie jest argument: nie można argumentować, że ludziom „jest dobrze”, kiedy nie mają wyboru.
bfontaine
Myślę, że mamy wtedy po prostu różne definicje „koniecznego”.
DarkWiiPlayer