Pomysł na element języka, loop-aware functions

0

Zastanawiam się czy została już gdzieś wykorzystana idea funkcji która byłaby świadoma tego że jest użyta w pętli - pozwoliłoby to na wydzielenie instrukcji sterujących przepływem kontroli (jak continue i break) do funkcji.

Należałoby wtedy oznaczyć funkcję jakimś modyfikatorem, typu loopable czy coś (tak jak można oznaczyć funkcję jako abstract czy static). Funkcję oznaczoną takim modyfikatorem możnaby użyć tylko bezpośrednio w pętli, albo innej funkcji która jest oznaczona jako loopable.

I jak by to miało działać, mniej więcej tak:

foreach ($values as $value) {
  if ($value === 1) {
    pass method();
  }
  throw new Exception();
}

loopable function callMethod(): int {
  if ($something) {
    continue; // pętla wyżej robi continue
  }
  if ($something) {
    break;  // pętla wyżej robi break
  }
  if ($something) {
    return 2; // pętla wyżej zwraca 2
  }
  throw new Exception();
}

Keyword pass byłby słowem kontrolującym flow (tak samo jak continue, break, return, throw), i robiłby wszystko to co robi funkcja po której się go zawoła. Czyli pass something(), to jeśli something() zrobi return, to pass robi return. Słowo pass miałoby znaczyć "Pass control of this flow to this method".

Oczywiście ma to swoje konsekwencje, czyli np nadużycie takiego modyfikatora - możnaby oznaczyć wszystkie funkcje w projekcie jako loopable, i wtedy po prostu tracimy statyczne sprawdzenie poprawności continue/break względem pętli, no ale jednak ten modyfikator miałby być używany tylko tam gdzie trzeba.

Wydaje mi się że mogą już istnieć języki które mają taki feature, znacie jakieś?

5

To nazywa się tenant's correspondence principle i jest kinda wspierane np. w Kotlinie w formie non-local returns.

Nie kojarzę żadnego języka, który pozwalałby wprost na wykonanie nielokalnego continue;, ale brzmi to trochę na system efektów algebraicznych, por. https://github.com/koka-lang/koka (https://www.microsoft.com/en-us/research/project/koka/), ponieważ efekty pozwalają właśnie na takie "skakanie w górę oraz dół" stacktrace'a.

W ramach ciekawostki dodam, że Rust podchodzi do tego w inny sposób - istnieje sobie taki typ jak ControlFlow, który pozwala funkcjom określać to, jak powinien zachować się ich caller; oczywiście to działa na best-effort basis i jeśli jakaś funkcja wyższego rzędu tego nie wspiera, no to magicznie nie będzie to działać - i tak przykładowo istnieje dedykowane Iterator::try_for_each(), ale już nie try_map().

1

Nie spotkałem się z rozwiązaniem tego typu i zasadniczo nie widzę zysku z dedykowanej konstrukcji w języku dla czegoś takiego. Na ogół, w takiej sytuacji, po prostu zwróciłbym krotkę, strukturę, Either, typu wyliczeniowego lub Option. Innym sposobem na zrobienie czegoś takiego jest użycie makra, tylko że wtedy nie można zrobić referencji na taką funkcję.

0
elwis napisał(a):

Nie spotkałem się z dedykowanym rozwiązaniem tego typu i zasadniczo nie widzę zysku z dedykowanej konstrukcji w języku dla czegoś takiego. Na ogół, w takiej sytuacji, po prostu zwróciłbym krotkę, dedykowaną strukturę, Either, typu wyliczeniowego lub Option. Innym sposobem na zrobienie czegoś takiego jest użycie makra, tylko że wtedy nie można zrobić referencji na taką funkcję.

No zysk jest bardzo prosty.

Mamy pętlę, w której są różne rodzaje kontroli przepływu:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
            continue;
        }
        if (\array_key_exists($error->code, $vagueReasons)) {
            throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
        }
        if ($error->code === 4) {
            break;
        }
        return 0;
    }
    throw new \Exception("Unexpected error level: $error->level");
}

W jednej pętli mamy if, continue, throw, break oraz return.

I teraz chciałbym wydzielić funkcję z tej pętli i może zrobić coś takiego:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania
    }
    throw new \Exception("Unexpected error level: $error->level");
}

function foo() {
  if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
      continue;
  }
  if (\array_key_exists($error->code, $vagueReasons)) {
      throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
  }
  if ($error->code === 4) {
      break;
  }
  return 0;
}

Oczywiście w aktualnych językach się tego nie da tak prosto zrobić, bo słowa continue, break oraz return muszą się pojawić w pętli. Jedyne co można zrobić to wyciągnąć return i throw do pod funkcji, ale te warunki które sprawdzają continue i break muszą zostać. Możesz oczywiście je poubierać w różne Either, Option, czy jakikolwiek ubranko chcesz; ale tak czy tak słowa continue i break w osobnych ifach muszą się pojawić w pętli, i to jest własnie mój problem.

Fajnie byłoby po prostu móc wziąć tą funkcję tak jak jest i wydzielić - po prostu. I nie widzę powodu czemu miałoby się nie dać móc tego zrobić. Jedynym powodem przeciw, jest - co jeśli taka funkcja byłaby wywołana poza pętlą - no trzeba jakoś to wymusić, i stąd pomysł na ten modyfikator. Albo po prostu błąd w runtime, jak kto woli.

2

scala 2 ma wbudowany mechanizm non-local returns, tzn. jeśli return jest lokalny to jest implementowany normalnie (tak jak w javie), a jeśli jest nielokalny to automatycznie kompilator dodaje rzucenie specjalnego wyjątku (w lambdzie) oraz blok try-catch (w metodzie zawierającej lambdę), który ten wyjątek interpretuje i wykonuje jako lokalny return. jest to oczywiście cieknąca abstrakcja, bo ten wyjątek może nie tylko być przechwycony np. przez jakąś monadkę do obsługi efektów ubocznych ale też może być (razem z otaczającym kodem) wykonany w innym wątku i wtedy return też nie zadziała jak trzeba.

scala 3 odziedziczyła mechanizm ze scali 2, ale oznaczyła do jako deprecated ( https://docs.scala-lang.org/scala3/reference/dropped-features/nonlocal-returns.html ). zamiast tego zalecane jest wprost używanie metod, które implementują analogiczny mechanizm. przykład z dokumentacji:

import scala.util.control.NonLocalReturns.*

extension [T](xs: List[T])
  def has(elem: T): Boolean = returning {
    for x <- xs do
      if x == elem then throwReturn(true)
    false
  }

@main def test(): Unit =
  val xs = List(1, 2, 3, 4, 5)
  assert(xs.has(2) == xs.contains(2))

mogę sobie wydzielić ciało tej funkcji lub jej kawałek, np:

import scala.util.control.NonLocalReturns.*

def extractedMethod[T](elem1: T, elem2: T)(using returner: ReturnThrowable[Boolean]): Unit =
  if elem1 == elem2 then throwReturn(true)

extension [T](xs: List[T])
  def has(elem: T): Boolean = returning {
    for x <- xs do
      extractedMethod(x, elem)
    false
  }

@main def test(): Unit =
  val xs = List(1, 2, 3, 4, 5)
  println(xs.has(2) == xs.contains(2))

w zależności od tego gdzie postawię returning może to emulować inne konstrukcje, np:

def method: String = returning { // otoczyłem całą metodę, więc throwReturn będzie działać jak non-local return
  ...
  whatever(arg1, arg2) { param3 =>
    ...
    if (cond4) then throwReturn("boom") // typ ma się zgadzać z tym zwracanym przez `returning`
    ...
  }
  ...
}

oraz

def method: String = {
  ...
  // otoczyłem tylko fora, więc throwReturn będzie działać jak break
  //   oczywiście pod warunkiem, że wyjątek zostanie rzucony i złapany tam gdzie trzeba (patrz mój komentarz o cieknącej abstrakcji)
  val x: Int = returning {
    for (x <- ...) {
      ...
      if (cond4) then throwReturn(83) // typ ma się zgadzać z tym zwracanym przez `returning`
      ...
    }
  }
  ...
}

mamy więc załatwione zarówno nielokalny break jak i return za pomocą returning + throwReturn. nielokalnego throw nie trzeba emulować, bo wyjątki standardowo lecą w górę stosu wywołań. zostaje więc emulacja continue. to można by emulować zwykłym booleanem zwracanym z wydzielonej metody.

returner w kodzie powyżej jest implicitem, ale można by go przerobić na argument expicit i dzięki temu łatwiej uzyskać wiele returnerów naraz, np. jeden do emulowania breaka, a drugi do emulowania nielokalnego returna.

co do łatania cieknących abstrakcji o których wspomniałem wyżej, odersky i świta pracują nad mechanizmem, który (jeśli teoria się ułoży i wszystko pyknie) będzie zapobiegał niewłaściwym użyciom non-local returns i innych rzeczy:
https://docs.scala-lang.org/scala3/reference/experimental/canthrow.html
https://docs.scala-lang.org/scala3/reference/experimental/cc.html

osobiście całe takie imperatywne kombinowanie wydaje mi się niepotrzebne, bo ja kombinowałbym funkcyjnie :) skoro mamy gotową strukturę danych do obrobienia to podejście funkcyjne moim zdaniem pasuje tu jak ulał.

zamiast czegoś takiego:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
            continue;
        }
        if (\array_key_exists($error->code, $vagueReasons)) {
            throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
        }
        if ($error->code === 4) {
            break;
        }
        return 0;
    }
    throw new \Exception("Unexpected error level: $error->level");
}

napisał bym pewnie coś a'la:

errors
  .filter(error => error.level == LIBXML_ERR_ERROR && error.code != 801) // XML_HTML_UNKNOWN_TAG
  // tap (a'ka inspect?) = coś pomiędzy map, a foreach, tzn. iterujemy, nie mapujemy, ale 
  // zwracamy oryginalną kolekcję, żeby mieć ten fluent style, czy jak się to tam zwie
  .tap { error =>
    if (\array_key_exists($error->code, $vagueReasons)) {
      throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
    }
  }
  // poniższe kombinacje da się załatwić prostszym w zrozumieniu kodem
  //   ale za to zachowują mniej więcej kolejność zgodną z oryginałem i pokazują (choć pokracznie) ten fluent style
  .takeWhile(_.error->code != 4)
  .headOption
  .map(_ => 0)
  .getOrElse(throw new \Exception("Unexpected error level: $error->level"))
}

:)
ps. oczywiście rzucanie wyjątków nie jest funkcyjne, ale z tym nie chciało mi się kombinować. to co jest powyżej powinno wystarczyć na potrzeby dyskusji.

aktualizacja (21:18):
wydaje mi się, że po kolejnej drobnej zmianie można by emulować continue także za pomocą returning + throwReturn, tzn.

def method: String = {
  ...
abstrakcji)
  for (x <- ...) {
    // otoczyłem tylko ciało fora, więc throwReturn będzie działać jak continue
    val x: Int = returning {
      ...
      if (cond4) then throwReturn(83) // typ ma się zgadzać z tym zwracanym przez `returning`
      ...
    }
  }
  ...
}

nie analizowałem tego przypadku długo, więc może nie do końca działa jak trzeba

10

Mamy pętlę, w której są różne rodzaje kontroli przepływu [...] W jednej pętli mamy if, continue, throw, break oraz return.

A tak to wydzielasz to do osobnej funkcji, gdzie nadal masz te same mechanizmy, tylko jest to schowane przed oglądającym. Zyskujesz pozorną przejrzystość, ale przeglądając kod musisz zajrzeć w kolejne miejsce, bo patrząc na samą pętlę nie masz zielonego pojęcia, w jaki sposób ma być wykonane niestandardowe sterowanie jej przepływem. Także - dla mnie to jest logika na zasadzie wyjmę śmieci spod zlewu i postawię przy drzwiach i już jest posprzątane :P

Pozorne zwiększenie czytelności, które tak naprawdę jedynie przenosi te "elementy" do innego miejsca, a do tego jest niebezpieczne - bo jak masz wpisane w pętli jawnie jakieś continue czy throw to patrząc na nią widzisz, co się może dziać. A wydzielenie tego do funkcji ukrywa taki scenariusz i jeśli ktoś nie wpadnie na pomysł, żeby zajrzeć w kolejne miejsce, to tego nie zauważy. Patrząc na foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania nie widzisz, czy to jest "zwykła" funkcja, czy twór loop-aware z bonusową niespodzianką - więc możesz zakładać, że nie będzie żadnych side-effect'ów jej odpalenia, a tu nagle jakiś wyjątek poleci :P

2
elwis napisał(a):

Nie spotkałem się z dedykowanym rozwiązaniem tego typu i zasadniczo nie widzę zysku z dedykowanej konstrukcji w języku dla czegoś takiego. Na ogół, w takiej sytuacji, po prostu zwróciłbym krotkę, dedykowaną strukturę, Either, typu wyliczeniowego lub Option. Innym sposobem na zrobienie czegoś takiego jest użycie makra, tylko że wtedy nie można zrobić referencji na taką funkcję.

Podoba mi się pomysł z Either bo tak jest w Haskellu :P Jest sobie funkcja loop :: (a -> Either a b) -> a -> b. Która przyjmuje lambdę a -> Either a b i jak z lambdy zwrócimy Right b to znaczy że koniec (odpowiednik return) a jak chcesz kolejny raz iterować to Left a (continue też tu wpada).

BTW nie dokońca rozumiem jaką sytuację biznesową chcesz rozróżnić za pomocą return i break. Co do exceptiona rozumiem. To się ogarnia w Haskellu kolejnym Either. W rezultacie mamy Either w Either czyli Either a (Either Error b) XD i trochę średnio to wygląda XD Można próbować zastąpić jeden Either za pomocą MonadError, No ale on tam w zasadzie dalej pozostaje

Może przydałby się dedykowany typ, a nie wciskanie wszędzie Either :D

0
Wibowit napisał(a):

osobiście całe takie imperatywne kombinowanie wydaje mi się niepotrzebne, bo ja kombinowałbym funkcyjnie :) skoro mamy gotową strukturę danych do obrobienia to podejście funkcyjne moim zdaniem pasuje tu jak ulał.

zamiast czegoś takiego:
[...]
napisał bym pewnie coś a'la:

errors
  .filter(error => error.level == LIBXML_ERR_ERROR && error.code != 801) // XML_HTML_UNKNOWN_TAG
  // tap (a'ka inspect?) = coś pomiędzy map, a foreach, tzn. iterujemy, nie mapujemy, ale 
  // zwracamy oryginalną kolekcję, żeby mieć ten fluent style, czy jak się to tam zwie
  .tap { error =>
    if (\array_key_exists($error->code, $vagueReasons)) {
      throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
    }
  }
  // poniższe kombinacje da się załatwić prostszym w zrozumieniu kodem
  //   ale za to zachowują mniej więcej kolejność zgodną z oryginałem i pokazują (choć pokracznie) ten fluent style
  .takeWhile(_.error->code != 4)
  .headOption
  .map(_ => 0)
  .getOrElse(throw new \Exception("Unexpected error level: $error->level"))
}

Tak, to jest dobra odpowiedź - faktycznie, może źle zadałem pytanie.

Przykład który podałem, to jednak prosta pętla z jednym poziomem zagnieżdżenia. Wyobraźmy sobie for'a, który ma w sobie wiele pod ifów, i te pod-ify, mają swoje pod ify i else'y, i różne body tych ifów mają różne kombinacje continue, else, return i throw.

Więc pojawia się pytanie, czy każdą pętle z dowolnie zagnieżdżonymi ifami i operacjami da się przerobić na takie użycie .filter(), .tap()? Czy może jednak da się to zrobić dla prostych forów (prostych, mam na myśli bez zagnieżdżeń), a dla zagnieżdżonych forum to się staje wykładniczo bardziej skomplikowane lub nie możliwe?

Oraz drugi temat: Cały sens tego wątku wziął się stąd, żeby wziąć bardzo skomplikowaną operacje na kolejkach, i podzielić ją na na pod metody. Wyobrażam sobie że z podejściem takim chainowanym (z .filter() i .tap()), owszem, możesz przerobić fora na taki chain, ale on nie stanie się przez to krótszy. Tzn jesli było 30 ifów w for, to teraz będzie 30 filtrów (zakładając że nie da się ich spłaszczyć do jednego). Bo myślę że obaj się zgadzamy że jak mamy długi chain operacji na kolekcji, to rozbijanie go na dwie funkcje mija się z celem?

Krok wstecz i big picutre

Weźmy pod uwagę syntax generatorów.

On się wziął stąd, że jak mamy iteracje, + if + populowanie jakiejś kolekcji, to zamiast:

$result = [];

foreach ($a as $b) {
  if ($something) {
    if ($something) {
      continue;
    } else {
      $result[] = $b
    };
    $call = $something;
    if ($call) {
      $result[] = $something;
    }
  }
}

możemy wynieść całą lub część takiego body funkcji do generatora, i wtedy dodawanie wartości do kolekcji (w tym wypadku $result[]) jest zamienione przez yield. Taki generator nie wie już do jakiej kolekcji coś dodaje (albo czy dodaje w ogóle, może tylko iteruje), cale cała kontrola przepływy (razem z ifami, zagnieżdżonymi for'ami, etc.) jest zachowana. To bardzo fajny sposób na wydzielenie kontroli przepływu z funkcji, i oddzielenie go od tego co robimy z iterowanymi elementami.

I teraz pytanie, czy taki generator również dałoby się zastąpic takim chainowanym podejściem? Myślę że do pewnego stopnia (jeśli nie ma zagnieżdżeń) to tak, dałoby się je zastąpić jakąś implementacją iteratora, ale dla bardziej zagnieżdżonych generatorów - nie wydaje mi się. Czy system kontroli przepływu (for, if, throw, return, break, continue) nie jest przypadkiem zbyt uniwersalnym narzędziem, żeby dało się go zastąpić takim runtime'owym rozwiązaniem jak właśnie chainowanie .filter(), .takeWhile()?

Dodakoto, chciałbym wyjaśnić - nie chcę mówić że chainowane podejście jest złe, albo gorsze od imperatywnego for'a. Zadaję tylko pytanie, czy na pewno chainowane podejście jest w stanie zrobić to samo co for'y?

Wibowit napisał(a):

zostaje więc emulacja continue. to można by emulować zwykłym booleanem zwracanym z wydzielonej metody.

No właśnie, dokładnie tego tyczy się cały wątek.

Bo w imperatywnym forze, też można sobie wydzielić funkcje, i zwracać boolean, żeby powiedzieć pętli wyżej "ej, pętlo, zrób mi tutaj continue". Tylko że taki boolean, musi oznaczać że (w obu podejściach, moim i Twoim), w pętli wyżej musi być if który złapie tego booleana i zrobi continue. I temu właśnie ma przeciwdziałać pomysł z loop-aware. Żeby ten if mógł zniknąć z pętli nadrzędnej i zostać schowany w wydzielonej metodzie.

0
cerrato napisał(a):

Mamy pętlę, w której są różne rodzaje kontroli przepływu [...] W jednej pętli mamy if, continue, throw, break oraz return.

A tak to wydzielasz to do osobnej funkcji, gdzie nadal masz te same mechanizmy, tylko jest to schowane przed oglądającym. Zyskujesz pozorną przejrzystość, ale przeglądając kod musisz zajrzeć w kolejne miejsce, bo patrząc na samą pętlę nie masz zielonego pojęcia, w jaki sposób ma być wykonane niestandardowe sterowanie jej przepływem. Także - dla mnie to jest logika na zasadzie wyjmę śmieci spod zlewu i postawię przy drzwiach i już jest posprzątane :P

Zgadzam się z Twoimi motywami! Mam takie same. Ale nie zgadzam się z tezą z postu.

Po pierwsze, nie tracę czytelności, bo wydzielona funkcja jest zaraz poniżej tej z której wydzielam, + wydzielona funkcja jest odpowiednio nazwana oraz ma odpowiednio nazwane argumenty. Jeśli z dużej funkcji na 30 linijek, można wydzielić 10 mniejszych, odpowiednio nazwanych funkcji prywatnych, i to zwiększa czytelność; to tak samo powinno się wydzielić część z pętli. Ja w swoich programach praktycznie zawsze rozbijam funkcje większe niż 5-6 linijek na mniejsze. Jeśli znalazłbym w jakimś progrmaie funkcje na 30 linijek, to na pewno chciałbym ją rozbić na mniejsze, i nie zgadzam się że jest to "chowanie" albo "ukrywanie czegoś" - to po prostu dzielenie kodu na mniejsze kawałki z odpowiednimi nazwami i argumentami.

cerrato napisał(a):

Pozorne zwiększenie czytelności, które tak naprawdę jedynie przenosi te "elementy" do innego miejsca, a do tego jest niebezpieczne - bo jak masz wpisane w pętli jawnie jakieś continue czy throw to patrząc na nią widzisz, co się może dziać. A wydzielenie tego do funkcji ukrywa taki scenariusz i jeśli ktoś nie wpadnie na pomysł, żeby zajrzeć w kolejne miejsce, to tego nie zauważy. Patrząc na foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania nie widzisz, czy to jest "zwykła" funkcja, czy twór loop-aware z bonusową niespodzianką - więc możesz zakładać, że nie będzie żadnych side-effect'ów jej odpalenia, a tu nagle jakiś wyjątek poleci :P

Tak i nie.

Niektóre rzeczy chcemy ukryć - niektóre nie. W szczególności, chcemy ukryć szczegóły implementacyjne jakiegoś rozwiązania, a uwidocznić decyzje biznesowe - i do tego właśnie chciałbym zastosować to rozwiązanie - jeśli coś jest szczegółem implementacyjnym, chciałbym to wynieść do funkcji o niższym poziomie, i pozwolić jej się tym zająć; a zostawić na górze pętli prawdziwe decyzje biznesowe.

Chociaż tez może nie powinienem użyć słowa "ukryć", chodzi o przeniesienie szczegółów na niższy poziom abstrakcji, i moim zdaniem wydzielenie tego do pod-metod jest dobrym pomysłem. Do tej pory udawało mi się to świetnie, jedyny problem jaki mam to wydzielenie czegoś z kodu który używa continue lub break :D

1

Zgadzam się z Twoimi motywami! Mam takie same

To tak z ciekawości - skoro uważasz podobnie, to skąd w ogóle pomysł na ten wątek?

bo wydzielona funkcja jest zaraz poniżej tej z której wydzielam

Ty tak ją umieścisz i super, ale jak zmusisz innych userów do takiego postępowania?

wydzielona funkcja jest odpowiednio nazwana oraz ma odpowiednio nazwane argumenty

Ty tak to ponazywasz i super, ale jak zmusisz innych userów do takiego postępowania?

chcemy ukryć szczegóły implementacyjne jakiegoś rozwiązania, a uwidocznić decyzje biznesowe

Tutaj jest kwestia rozgraniczenia tego, gdzie jest podział między tym, co jest warte pokazania, a co ukryć. I nie chodzi mi o podział biznes/implementacja, tylko raczej o to, jakie rzeczy warto pokazać, a co ukryć. Side-effect funkcji w postaci zmiany przepływu (jakiś wyjątek czy break) jest na tyle niestandardowy i potencjalnie niebezpieczny, że ja bym tego w żadnym razie nie ukrywał.

2

w pierwszej kolejności czytając post to mi się cobol przypomina, perform i go to.
Metody wydzielone z pętli czasami będą używane poza pętlą i co w takim przypadku, mam sobie inną napisać?

Dalej, w pierwszej kolejności bym pomyślał na użyciem kodu który nie będzie miał break/continue np inna pętla/inny warunek, tzn refaktoring tak aby w pętli był tylko kod od pętli i jej sterowaniem a reszta w metodach.

A kolejna rzecz która mi się nie podoba, to funkcja tutaj ma bardzo duży efekt uboczny, który nie jest wartością zwracaną, wartością zmienionych parametrów etc.

0

Nie rozumiałem o co chodziło do drugiego postu.

Ja bym się zanurzył do poziomu procesora, zwykle instrukcje takie jak continue i break implementowane są jako zwykły jmp do instrukcji czy to sprawdzania warunków wyjścia przy continue lub break za pętlę.

Mając zewnętrzną funkcję, tworzymy ramkę stosu przez co musimy tą ramkę wyczyścić, wrócić i wywołać jmp, ewentualnie by było wyczyszczenie ramki i jump absolutny, to jest ogólnie jeszcze gorsze od goto.
Bo goto umożliwia jumpy tylko w obrębie funkcji.

Można by było odwrócić zależności zamiast funkcja wywołuje tą callmethod, to ta call method jest pętlą, a wywoływane są funkcję, które określają czy ma być break czy continue, który potem się wykona.

Jak się opakuje w obiekty, to pewnie przy 1-2 instrukcyjnym nakładzie się to i tak da zaimplementować.

A w takim c/c++ można zrobić funkcję wstrzykiwaną przez preprocesor, ogólnie to zwykle się robi funkcję lub obiekty, czystych bloków kodu nie wiem jak wykonać w templatach, ale na makro można takie coś łatwo uzyskać.

Coś jak inline code.
https://godbolt.org/z/v1jKeWYdh

0
cerrato napisał(a):

Zgadzam się z Twoimi motywami! Mam takie same

To tak z ciekawości - skoro uważasz podobnie, to skąd w ogóle pomysł na ten wątek?

Zgadzam się z motywami, że chowanie szamba i śmieci to nie jest dobry powód - lepiej posprzątać należycie. Z tym się zgadzam.

cerrato napisał(a):

bo wydzielona funkcja jest zaraz poniżej tej z której wydzielam

Ty tak ją umieścisz i super, ale jak zmusisz innych userów do takiego postępowania?

wydzielona funkcja jest odpowiednio nazwana oraz ma odpowiednio nazwane argumenty

Ty tak to ponazywasz i super, ale jak zmusisz innych userów do takiego postępowania?

cerrato napisał(a):

chcemy ukryć szczegóły implementacyjne jakiegoś rozwiązania, a uwidocznić decyzje biznesowe

Tutaj jest kwestia rozgraniczenia tego, gdzie jest podział między tym, co jest warte pokazania, a co ukryć. I nie chodzi mi o podział biznes/implementacja, tylko raczej o to, jakie rzeczy warto pokazać, a co ukryć. Side-effect funkcji w postaci zmiany przepływu (jakiś wyjątek czy break) jest na tyle niestandardowy i potencjalnie niebezpieczny, że ja bym tego w żadnym razie nie ukrywał.

To wszystko jest temat na inną rozmowę - nt wynoszenia mniejszych funkcji z dużych. Bo tematy które poruszasz są bardzo ważne; ale one występują również w przypadku zwykłych metod i funkcji. Temat z rodziny "Clean code", a ten wątek dotyczy oddania kontroli przepływu do pod-method. Możemy założyć osobny wątek pod to :> Chętnie się wypowiem.

moskitek napisał(a):

w pierwszej kolejności czytając post to mi się cobol przypomina, perform i go to.

No nie no, zupełnie inny temat. Goto umie skakać do dowolnego fragmentu w kodzie - co jest mega niebezpieczne. continue/break w podfunkcji, jedyne co może zrobić to wyjść z pętli bezpośrednio nad sobą, tak jak każde inne continue.

moskitek napisał(a):

Metody wydzielone z pętli czasami będą używane poza pętlą i co w takim przypadku, mam sobie inną napisać?

Jeśli metody wydzielone z pętli nie używają continue ani break, to nie musisz - wystarczy wtedy jedna i nie musisz pisać drugiej. Jeśli jednak używają continue i break, to nie mógłbyś ich wywołać poza pętlą.

moskitek napisał(a):

Dalej, w pierwszej kolejności bym pomyślał na użyciem kodu który nie będzie miał break/continue np inna pętla/inny warunek, tzn refaktoring tak aby w pętli był tylko kod od pętli i jej sterowaniem a reszta w metodach.

No, każdy logicznie myślący człowiek by tak zrobił, ja również. W wielu przypadkach da się tak zrobić, i tak właśnie robię.

Niestety, jest też spora ilość przypadków, kiedy nie da się tego zrobić, i stąd ten wątek.

moskitek napisał(a):

A kolejna rzecz która mi się nie podoba, to funkcja tutaj ma bardzo duży efekt uboczny, który nie jest wartością zwracaną, wartością zmienionych parametrów etc.

Tak samo jak generatory.

0

Znaczy może goto skakać do dowolnego fragmentu w danej funkcji, bo będąc w innej musiało by wiedzieć jak posprzątać stos inaczej doszło by do crashu aplikacji, raz mogło by goto mieć ramkę main->callmethod, a innym razem main->dupa->callmethod, w obu przypadkach skok by występował do main, a main ma potem return, który tylko raz zdejmuje ze stosu adres, a w przypadku pierwszym i drugim musiałby odpowiednio raz i 2 razy zdjąć adres jako śmieci i dopiero by się dobrał do właściwego adresu, w ogóle są jeszcze kanarki, zabezpieczenie na stosie.

0
CloudPro napisał(a):

Znaczy może goto skakać do dowolnego fragmentu w danej funkcji, bo będąc w innej musiało by wiedzieć jak posprzątać stos inaczej doszło by do crashu aplikacji, raz mogło by goto mieć ramkę main->callmethod, a innym razem main->dupa->callmethod, w obu przypadkach skok by występował do main, a main ma potem return, który tylko raz zdejmuje ze stosu adres, a w przypadku pierwszym i drugim musiałby odpowiednio raz i 2 razy zdjąć adres jako śmieci i dopiero by się dobrał do właściwego adresu, w ogóle są jeszcze kanarki, zabezpieczenie na stosie.

Tak to prawda; ale nawet pomijając te wszystkie argumenty, to pozostaje inny, gorszy, killer argument:

kod napisany z goto jest niemal niemożliwy do utrzymania - nie dlatego że trzeba pamiętać o sprzataniu stosu; ale dlatego że modelowanie takie skomplikowanego systemu w głowie żeby nad nim pracować przekracza zdolności kognitywne chyba każdego człowieka na świecie.

0

@Riddle: czasem malware tak się pisze, że nie ma tam ani jednej funkcji, potem ma się jednego entrypoint _start, który sam się modyfikuje i skacze w dowolne miejsca, poprzednio operacja dodawania może być wynikiem następnej instrukcji procesora, automatyczne analizatory wymiękają bo nie idzie grafu z tego uzyskać.

1

Mając zewnętrzną funkcję, tworzymy ramkę stosu przez co musimy tą ramkę wyczyścić, wrócić i wywołać jmp, ewentualnie by było wyczyszczenie ramki i jump absolutny, to jest ogólnie jeszcze gorsze od goto.

Zauważ, że dokładnie to samo ma miejsce robiąc throw / catch, a jednak to drugie nie przykuwa tak dużej uwagi;

1

Właśnie wymysliliście yield

1
Riddle napisał(a):

No zysk jest bardzo prosty.

Mamy pętlę, w której są różne rodzaje kontroli przepływu:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
            continue;
        }
        if (\array_key_exists($error->code, $vagueReasons)) {
            throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
        }
        if ($error->code === 4) {
            break;
        }
        return 0;
    }
    throw new \Exception("Unexpected error level: $error->level");
}

Ten kod to tak na poważnie? Przecież to nie ma żadnego sensu. Dlaczego tworzysz jakąś tablicę $errors zamiast przerwać przetwarzanie, jeśli błąd będzie tak krytyczny, że nie chcesz patrzeć na resztę? Przecież to co tu się dzieje zależy tylko i wyłącznie od wartości $error, więc czemu te działania nie są częścią implementacji typu, którego jest error? Zamiast stosować jakieś dziwne poziomy i kody błędów, może lepiej byłoby identyfikować błąd za pomocą typu?

Zakładając, źe zasadna w ogóle jest ta pętla to dążyłbym do kodu tego typu:

foreach($errors as $error) {
  if($error->expected_level(LIBXML_ERR_ERROR)
           ->unacceptable_codes($vagueReasons)
           ->isFatal())
      break;
}

Przypadek z continue traci sens, bo użyłeś go tylko po to, żeby to rzucenie wyjątku na końcu nie musiało być w else.

0
elwis napisał(a):
Riddle napisał(a):

No zysk jest bardzo prosty.

Mamy pętlę, w której są różne rodzaje kontroli przepływu:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
            continue;
        }
        if (\array_key_exists($error->code, $vagueReasons)) {
            throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
        }
        if ($error->code === 4) {
            break;
        }
        return 0;
    }
    throw new \Exception("Unexpected error level: $error->level");
}

Ten kod to tak na poważnie? Przecież to nie ma żadnego sensu. Dlaczego tworzysz jakąś tablicę $errors zamiast przerwać przetwarzanie, jeśli błąd będzie tak krytyczny, że nie chcesz patrzeć na resztę? Przecież to co tu się dzieje zależy tylko i wyłącznie od wartości $error, więc czemu te działania nie są częścią implementacji typu, którego jest error? Zamiast stosować jakieś dziwne poziomy i kody błędów, może lepiej byłoby identyfikować błąd za pomocą typu?

Nie wiem dlatego w ogóle wyciągasz ten temat, skoro to jest tylko i wyłącznie kod poglądowy, mający przedstawić zagnieżdżone ify i kontrole przepływu - faktyczna implementacja nie ma znaczenia. Jak chcesz to podmienie ten kod pogladowy inną implementacją, ale to nie ma znaczenia.

elwis napisał(a):

Zakładając, źe zasadna w ogóle jest ta pętla to dążyłbym do kodu tego typu:

foreach($errors as $error) {
  if($error->expected_level(LIBXML_ERR_ERROR)
           ->unacceptable_codes($vagueReasons)
           ->isFatal())
      break;
}

Przypadek z continue traci sens, bo użyłeś go tylko po to, żeby to rzucenie wyjątku na końcu nie musiało być w else.

Okej, czyli próbujesz obejść problem i zniewlować w ogóle continue - może być, tylko to nie jest odpowiadanie na temat, tylko omijanie tematu. Jak chcesz to znowu - mogę napisać poglądowy kawałek kodu, w którym ciężko będzie usunąć continue w tak prosty sposób.

A nawet jeśli miałbyś rację; to nadal temat pozostaje, bo nadal istnieje ten if w pęli, na którym się robi break, czyli zaprezentowałeś dokładnie to o czym pisał @Wibowit z przekazaniem booleana. Powiedziałeś też to samo o czym pisał @moskitek - czyli zrefaktorować kod, tak żeby potrzeby na takie loop-aware funkcje nie było. Owszem, w przykłądach kiedy da się to zrobić, to powinno się to zrobic.

Ale ten wątek dotyczy tych przypadków, kiedy nie da się tak prosto tego zrefaktorować.

Argument rozumiem: przyjmujesz propozycje, i odrzucasz ją, prezentując sposób w jaki sposób da się rozwiązać problem bez tej propozycji. No spoko; ale to samo można powiedzieć o generatorach - generatory też nic sobą takiego nie wnoszą, czego byś nie mógł zaimplementować zwykłymi klasami, ale jednak są elementem języka bo trochę naturalniej rozwiązują jakiś problem.

4
Riddle napisał(a):

Okej, czyli próbujesz obejść problem i zniewlować w ogóle continue - może być, tylko to nie jest odpowiadanie na temat, tylko omijanie tematu. Jak chcesz to znowu - mogę napisać poglądowy kawałek kodu, w którym ciężko będzie usunąć continue w tak prosty sposób.

Po prostu moja teza jest taka, że próbujesz rozwiązywać problemy, które wynikają ze słabej jakości kodu.Tak więc powinieneś pokazać realny przykład, w którym jest to zasadne. Tylko tak udowodnisz, że twój pomysł ma sens, a nie wystarczy po prostu nauczyć się pisać dobry kod. Język programowania nie powinien ułatwiać pisania złego kodu (ja nawet lubię jeśli na to nie pozwala)

0
elwis napisał(a):
Riddle napisał(a):

Okej, czyli próbujesz obejść problem i zniewlować w ogóle continue - może być, tylko to nie jest odpowiadanie na temat, tylko omijanie tematu. Jak chcesz to znowu - mogę napisać poglądowy kawałek kodu, w którym ciężko będzie usunąć continue w tak prosty sposób.

Po prostu moja teza jest taka, że próbujesz rozwiązywać problemy, które wynikają ze słabej jakości kodu, tak więc powinieneś pokazać realny przykład, w którym jest to zasadne, żeby udowodnić, że twój pomysł ma sens, a nie wystarczy po prostu nauczyć się pisać dobry kod.

Mogę ostrożnie założyć, że jestem jedną z osób które cenią czystość kodu ponad wszystko w aplikacjach, i właśnie czystością kodu kieruję się rzucając tą propozycję. "Clean code" to moja biblia. Zacząłem zauważać problemy z niemożliwością wydzielenia continue i break do pod funkcji jakieś 5 lat temu; i zawsze starałem się to ładnie i czysto zaprezentować, czasem udało mi się uprościć problem niewlując continue, czasem przerabiałem to na generator, czasem udało się to przerobić na polimorfizm, czasem na rekruencję, czasem wydzielić DSL, czasem zwrócić either lub optional. Ale raz na jakiś czas zostawałem z problemem, gdzie nie dało się go łatwo zrefaktorować, tak żeby nie pogorszyć problemu. Zauważyłem taki problem kilkanaście razy w ciągu ostatnich 5 lat można powiedzieć.

Mnogość wielu przełączników kontroli wersji, owszem, może oznaczać zły kod, ale nie koniecznie. Czasem normalnie napisany kod również po prostu będzie miał wiele breaków i continue, i nie oznacza to wcale że jest to słabej jakości kod.

A co do realnego przykładu, to bardzo chętnie pokazałbym Ci taki przykład, bo mam ich kilka, w których moim zdaniem użycie imperatywnego fora razem z continue i break byłoby zasadne, ale obawiam się co się stanie jak go tutaj wrzucę. A spodziewam się tego, że ten kod zostanie wzięty, i przerobiony albo na chainy, albo na polimorfizm, albo na option i eithery, albo na dowolny inny sposób zostanie przerobiony usuwając continue i break, i nie ma z tym nic złego; oprócz tego że ja też kiedy próbowałem "wyczyścić" ten kod, próbowałem już robić wszystkie te opcje, i za każdym razem doszedłem do wnisoku, że tak przerobiony kod jest gorszy niż był; i najczyściejszy jaki może być w danym języku to właśnie z continue i break. Wiem że niektórym się to może niemiesić w głowie: "coo, jak to kod z takim araicznym modelem jak foreach może być czysty?", ale tak właśnie uważam. Czasem dobrym rozwiązaniem problemu są kolekcje, czasem streamy, czasem optionale, a czasem zwykłe fory.

I jeszcze odniosę się do tego kawałka, konkretnie:

elwis napisał(a):

Po prostu moja teza jest taka, że próbujesz rozwiązywać problemy, które wynikają ze słabej jakości kodu

Czemu konkretnie kod, który ma wiele instrukcji kontroli przepływu (jak if, break, continue, return) miałoby oznaczać słabą jakość kodu? Czy nie jest tak, że zawsze kiedy piszesz skomplikowany mechanizm (jak parser, rendered, jakąś bibliltekę niskiego poziomu), to zawsze pojawiają się miejsca gdzie pojawia się taka skomplikowana logika, którą w jakis sposób trzeba zakodzić, i czasem ją się uda wydzielić do odpowiednich klas, a czasem nie? Bo wiadomo, że jak piszemy aplikację webową w jakims framework'u, to takie sytuacje nigdy nie zachodzą, bo zawsze da się ładniej zaprezentować problem. Ale jak piszemy jakiś sterownik, albo tool bez frameworka, który ogarnia masę skomplikowanych inputów, to czasem mnogość breaków i contiue to jest normalna rzecz. Moim zdaniem wyniesienie niektórych continue i break do pod funkcji byłoby dobrym sposobem organizacji czystego kodu; i konieczność użycia go nie świadczyłaby o słabej jakości kodzie.

1

@elwis: Przykład kodu, w którym ciężko byłoby usunąć continue:

foreach ($values as $value) {
  if ($a) {
      call1();
      if ($b) {
          call2();
          if ($c) {
              call3();
              if ($d) {
                  call4();
                  continue;
              }
          }
          call5();
      }
  }
  call6();
}

W takim przykładzie, continue nie da się łatwo ani usunąć, ani zamienić na else. Wyjście jakie masz to:

  • albo zmienić continue na boolean'a, co jest średnie
  • albo zrefaktorować te if z elsami, tylko wtedy wywołania call5() i call6() będą zduplikowane (co może nie być czyste, z uwagi )
  • albo próbować zareprezentować ten chain kontroli jakimś obiektem.

Ogólnie zaczynam trochę kumać @elwis o co Ci chodziło z czystością kodu, bo zauważyłem pewną zalezność. Jeśli każda linijka w pętli zaczyna się słowem return, throw, if, continue albo break, tzn każda linijka kontroluje przepływ, i nic po niej się nie wykona, to masz rację - każdą taką pętlę da się zrefaktorować do czystszego kodu, i funkcje loop-aware nie są potrzebne. Ale jeśli linijki nie zaczynają się keywordami kontrolującymi przepływ, i calle sobie po prostu falldown'ują, to wtedy nie zawsze da się to wynieść, i wtedy funkcja loop-aware może być przydatna do wydzielenia tego.

2

I teraz chciałbym wydzielić funkcję z tej pętli i może zrobić coś takiego:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania
    }
    throw new \Exception("Unexpected error level: $error->level");
}

function foo() {
  if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
      continue;
  }
  if (\array_key_exists($error->code, $vagueReasons)) {
      throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
  }
  if ($error->code === 4) {
      break;
  }
  return 0;
}

Co my tu mamy

voidoza, brak typów, ekscepcje ( :D )

no tak, jeżeli ktoś piszę taki kod, to faktycznie może trzeba dodać do języków takie featuresy jak loop-awarness

Na początku jak zobaczyłem ten wątek to myślałem że chodzi o optymalizację typu N+1 gdzie takie coś mogłoby sprawić że inne API byłoby wołane i mi się to spodobało, ale jeżeli przedstawiasz to jako rozwiązanie na kod który nie nawet nie próbuje podchodzić do obsługi błędów tak jak powinien, no to ciężko mi to kupić.

niech foo zwraca odpowiednie błędy i niech caller code decyduje co z nimi robi.

Czemu konkretnie kod, który ma wiele instrukcji kontroli przepływu (jak if, break, continue, return) miałoby oznaczać słabą jakość kodu? Czy nie jest tak, że zawsze kiedy piszesz skomplikowany mechanizm (jak parser, rendered, jakąś bibliltekę niskiego poziomu), to zawsze pojawiają się miejsca gdzie pojawia się taka skomplikowana logika, którą w jakis sposób trzeba zakodzić, i czasem ją się uda wydzielić do odpowiednich klas, a czasem nie?

Absolutnie nie ma nic złego w ifach, breakach, continue i returnach lub nawet goto, są to basic control flow primitives.

Za to jest wiele złego w braku obsługi błędów jak w w/w przypadku.

foreach ($values as $value) {
	switch(Blabla($value))
	{
		Result.Continue => continue;
        Result.Ok => Print("dupa");
	}
}


function SomeResult Blabla($value)
{
  if ($a) {
      call1();
      if ($b) {
          call2();
          if ($c) {
              call3();
              if ($d) {
                  call4();
                  return Result.Continue;
              }
          }
          call5();
      }
  }
  call6();
  
  return Result.OK;
}
0

@cerrato: No i widzisz? Dokładnie o to mi chodziło

WeiXiao napisał(a):
foreach ($values as $value) {
	var result = Blabla($value);
	
	if (result == Result.Continue)
	{
		continue;
	}
	else if (result == Result.OK)
	{
        Print("dupa");
	}
}


function SomeResult Blabla($value)
{
  if ($a) {
      call1();
      if ($b) {
          call2();
          if ($c) {
              call3();
              if ($d) {
                  call4();
                  return Result.Continue;
              }
          }
          call5();
      }
  }
  call6();
  
  return Result.OK;
}
  • Nadal masz if w nadrzednej pętli
  • Blabla może zwrócić jakąś wartość, i wtedy musiałbyś opakować swój wynik w coś, albo Optional albo Either, rozwiązanie proponowane już 3 razy w tym wątku, i nie odpowiada na temat.
  • Poza tym, wcale nie wiem czy return Result.Continue; jest czystszy niż po prostu sam continue.

Jednak bardzo dziękuję za udział w rozmowie. Cały temat z funkcjami loop-aware to żeby nie było ifa w nadrzędnej pętli, i żeby możnabyło po prostu wydzielić funkcje bez dodawania dodatkowych wartości jak ten enum Result albo Optional.

WeiXiao napisał(a):

I teraz chciałbym wydzielić funkcję z tej pętli i może zrobić coś takiego:

foreach ($errors as $error) {
    if ($error->level === LIBXML_ERR_ERROR) {
        foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania
    }
    throw new \Exception("Unexpected error level: $error->level");
}

function foo() {
  if ($error->code === 801) { // XML_HTML_UNKNOWN_TAG 
      continue;
  }
  if (\array_key_exists($error->code, $vagueReasons)) {
      throw new MalformedContentException($vagueReasons[$error->code] . " on line $error->line");
  }
  if ($error->code === 4) {
      break;
  }
  return 0;
}

Co my tu mamy

voidoza, brak typów, ekscepcje ( :D )

no tak, jeżeli ktoś piszę taki kod, to faktycznie może trzeba dodać do języków takie featuresy jak loop-awarness

Odnosiłem się już do tego argumentu wcześniej. To jest kod poglądowy, mający prezentować tylko i wyłącznie kontrolę przepływu, faktyczna implementacja nie ma znaczenia.

Teraz patrz, uwaga uwaga, zmieniam nazwy zmiennych $error na $gameTile, i BAM! Coś takiego, kod już nie dotyczy obsługi błędów. Coś takiego!

foreach ($gameTiles as $tile) {
    if ($tile->isBlockTile()) {
        foo(); // funkcja robi continue, break, return albo throw, zależnie od swojego wykonania
    }
    throw new \Exception("Unexpected block tile");
}

function foo() {
  if ($tile->neighbours === 1) { // NEIGHTBOUR_LIMIT
      continue;
  }
  if (\array_key_exists($tile->firstNeighbour, $vagueReasons)) {
      throw new UnexpectedVagueNeighbour($vagueReasons[$tile->firstNeighbour] . " on position $tile->position");
  }
  if ($tile->height === 4) {
      break;
  }
  return 0;
}

Powtarzam jeszcze raz, kod jest tylko poglądowy i jego głównym celem jest prezentacja kontroli przepływu. @cerrato dokładnie o to mi chodziło w komentarzu którym pisałem. Odpowiadający zwracają uwagę na nieistotne szczegóły kodu poglądowego w kontekście tego czy teza z wątku jest poprawna czy nie.

0

Napisałem ci Wrapper

new _4pStateMachine<int>(new List<int> { 1, 2, 3, 4, 5 }, CrazyAsHellLogic).Iterate();

public static (State State, Context? Ctx) CrazyAsHellLogic(int x)
{
    if (x == 1)
        return (State.Continue, null);

    if (x == 2)
    {
        Console.WriteLine("dupa");
        return (State.JustMove, null);
    }

    if (x == 4)
        return (State.Throw, new Context { Throw = () => throw new NotImplementedException("asd") });

    if (x == 5)
        return (State.Break, null);

    return (State.JustMove, null);
}


public class _4pStateMachine<T>
{
    private List<T> _list;

    private Func<T, (State State, Context? Ctx)> func;

    public _4pStateMachine(List<T> list, Func<T, (State State, Context? Ctx)> f)
    {
        _list = list;
        func = f;
    }

    public void Iterate()
    {
        foreach (var item in _list)
        {
            Console.WriteLine(item);
            
            var result = func(item);
            
            if (result.State == State.Continue || result.State == State.JustMove)
                continue;
            else if (result.State == State.Break)
                break;
            else if (result.State == State.Throw)
                result.Ctx.Throw();
            else if (result.State == State.Return)
                return;
        }
    }
}

public enum State { Break, Continue, JustMove, Throw, Return }

public class Context
{
    public Action Throw { get; set; }
}

Prints

1
2
dupa
3
4
Unhandled exception. System.NotImplementedException: asd
0
WeiXiao napisał(a):

Napisałem ci Wrapper

[...]

Chciałbym ponownie powiedzieć:

Ja znam te wszystkie sposoby jak można obejść problemy z wydzieleniem continue i break. Wiem że można wydzielić obiekt, zwrócić Optional, Etiher, napisać wrapper, zamienić to na polimorfizm, napisać macro które to ogarnie, użyć generatora, iteratora, statusów kodu, booleanów, enumów, Resultów, znam to wszystko i próbowałem. Pytanie z wątku nie brzmi: "Jakie są alternatywy dla continue w pętli". Także dziękuję wszystkim za odpowiedzi, ale nie o to chodziło.

Pytanie z wątku prezentuje pewną propozycję nowego sposobu kontroli przepływu, i chciałem usłyszeć opinie na jego temat. Na razie dostałem odpowiedzi, jak to obejść, ale to całkowicie ignoruje postawione pytanie. W gruncie rzeczy, całą Twoją odpowiedź (w kontekście tego tematu) @WeiXiao można by podsumować tym stwierdzeniem: @Riddle nie sądzę że taki element języka ma zastosowanie, bo można to załatwić w inny sposób. I tyle. To wystarczyłoby żeby wyczerpać wasze odpowiedzi; bo pytanie nigdy nie dotyczyło tego jak to obejść.

To co ta odpowiedź robi, to mówi: "popatrz jaką mam alternatywę: <wstaw wrapper, result, optional, inne>, a więc Twoja propozycja nie ma zastosowania". Super, dzięki, znałem te alternatywy już wcześniej; ale nie tego dotyczy wątek. Jedyne osoby które rzuciły senswoną odpowiedź komentując temat to @Patryk27 i @Wibowit.

1

@Riddle:

I to jest lepsze od oznaczenia funkcji jako loopable... w jaki sposób konkretnie?

np. taki, że możesz użyć go już teraz :P

A tak na serio, to uważam że twój pomysł ma sens, aczkolwiek ma jedną wadę

Z góry (caller's perspective) nie widać co robi funkcja.

np. robię sobie

for (int i=0; i<list.Count; i++)
{
	if (i == 2)
		break;

	DoSomething(list[i]);
}

i może się okazać że DoSomething mi robi breaka wcześniej i ciężko to zauważyć.

Dodatkowo refactorowanie takiego kodu jest ciężkie, bo łatwo zmienić zachowanie w kodzie wyżej, a taki problem nie występuje gdy mamy zwykłe returny (imo).

0

Może taki przykład zilustruje problem lepiej? (pseudokod)

Załóżmy, że mamy pewien interfejs na klienta HTTP:

interface HttpClient {
    fn perform(self, request: HttpRequest) -> Result<HttpResponse>;
}

... który posiada podstawową, bazową implementację, po prostu odpalającą przekazane zapytanie:

class BasicHttpClient implements HttpCllient {
    fn perform(self, request: HttpRequest) -> Result<HttpResponse> {
        /* ... */
    }
}

Jako że serwisy bywają anrelajabyl, pierwszy dzień na produkcji kończy się nową klasą (mmm kompozycja chain of responsibility whoa):

class RetryingHttpClient implements HttpClient {
    fn new(parent: HttpClient) -> Self {
        Self { parent }
    }
    
    fn perform(self, request: HttpRequest) -> Result<HttpResponse> {
        for try_id in 0..10 {
            let response = self.parent.request(request);
            
            if response.is_ok() {
                return response;
            }
        }
        
        return Error("too many tries");
    }
}

I teraz tak - kto tutaj odpowiada za obsługę kodów HTTP -- tzn. czy HTTP 4xx / 5xx to soft-failure (i można spróbować ponownie), czy raczej hard-failure?

Mając taki setup, dosyć łatwo sobie wyobrazić możliwe rozwiązanie oparte o "loopable" funkcje:

class RetryingHttpClient implements HttpClient {
    fn new(parent: HttpClient) -> Self {
        Self { parent }
    }

    virtual loopable fn on_response(response: HttpResponse) {
        // no-op
    }
    
    fn perform(self, request: HttpRequest) -> Result<HttpResponse> {
        for try_id in 0..10 {
            let response = self.parent.request(request);
            
            self.on_response(response);
            
            if response.is_ok() {
                return response;
            }
        }
        
        return Error("too many tries");
    }
}

class ServiceAClient extends RetryingHttpClient {
    loopable fn on_response(response: HttpResponse) {
        if response.code == 500 {
            /* wiemy, że ten konkretny serwis zachowuje się śmiesznie - sleep 5s i: */
            continue;
        }
    }
}

class ServiceBClient extends RetryingHttpClient {
    loopable fn on_response(response: HttpResponse) {
        if response.code == 500 {
            /* wiemy, że dla tego serwisu HTTP 500 to over, zatem: */
            break;
        }
    }
}

Nie jest to jedyne możliwe rozwiązanie i zdecydowanie nie jest najlepsze (zwłaszcza, że istnieje jedynie hipotetycznie) - ale z wieloma różnymi formami tego problemu / tego patternu miałem już do czynienia i wbrew pozorom nie jest to coś tak bardzo abstrakcyjnego; a że ludzie często robią po prostu rzeczy w stylu virtual fn should_retry(HttpResponse) -> bool no to cóż, być może potrzeba wysokopoziomowego sterowania control flow z delegatów po prostu nie rzuca się w oczy tak jak powinna :-P

0
WeiXiao napisał(a):

Dodatkowo refactorowanie takiego kodu jest ciężkie, bo łatwo zmienić zachowanie w kodzie wyżej, a taki problem nie występuje gdy mamy zwykłe returny (imo).

Nie zgadzam się, bo nawet jak masz zwykłe returny, to nadal dodanie bugów poprzez refaktor jest bardzo proste (chyba że masz odpowiednie testy, ale jeśli je masz, to refaktorowanie mojego przykładu też jest proste).

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