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 break
a, a drugi do emulowania nielokalnego return
a.
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