Jak wyjść z pętli w Scali?


276

Jak przerwać pętlę?

var largest=0
for(i<-999 to 1 by -1) {
    for (j<-i to 1 by -1) {
        val product=i*j
        if (largest>product)
            // I want to break out here
        else
           if(product.toString.equals(product.toString.reverse))
              largest=largest max product
    }
}

Jak przekształcić zagnieżdżone pętle w rekurencję ogona?

Z Scala Talk na FOSDEM 2009 http://www.slideshare.net/Odersky/fosdem-2009-1013261 na 22 stronie:

Przerwij i kontynuuj Scala ich nie ma. Czemu? Są nieco konieczne; lepiej korzystać z wielu mniejszych funkcji Problem z interakcją z zamknięciami. Nie są potrzebne!

Jakie jest wyjaśnienie?


Twoje porównanie wymaga drugiego znaku równości: if (product.toString == product.toString.reverse) lub może wywołania metody equals-Method.
użytkownik nieznany

tak, tęskniłem za tym, kiedy go wpisywałem
TiansHUo

Wiem, że wskrzeszam stare pytanie, ale chciałbym wiedzieć, jaki jest cel tego kodu? Najpierw pomyślałem, że starałeś się znaleźć największy możliwy produkt „palindrom” z podanymi kombinacjami ii j. Jeśli ten kod działa do końca bez wyłamania się z pętli, wynikiem jest wcześniejsze 906609wyłamanie się z pętli, więc wynik jest 90909taki, że wyłamanie się z pętli nie czyni kodu „bardziej wydajnym”, ponieważ zmienia wynik.
Ryan H.,

Odpowiedzi:


371

Masz trzy (lub mniej więcej) opcje wyjścia z pętli.

Załóżmy, że chcesz sumować liczby, aż suma będzie większa niż 1000. Próbujesz

var sum = 0
for (i <- 0 to 1000) sum += i

z wyjątkiem tego, że chcesz zatrzymać kiedy (suma> 1000).

Co robić? Istnieje kilka opcji.

(1a) Użyj konstrukcji, która zawiera testowany warunek.

var sum = 0
(0 to 1000).iterator.takeWhile(_ => sum < 1000).foreach(i => sum+=i)

(ostrzeżenie - zależy to od szczegółów przeplatania testu takeWhile i foreach podczas oceny i prawdopodobnie nie powinno się go stosować w praktyce!).

(1b) Użyj rekurencji ogona zamiast pętli for, wykorzystując łatwość napisania nowej metody w Scali:

var sum = 0
def addTo(i: Int, max: Int) {
  sum += i; if (sum < max) addTo(i+1,max)
}
addTo(0,1000)

(1c) Wróć do korzystania z pętli while

var sum = 0
var i = 0
while (i <= 1000 && sum <= 1000) { sum += 1; i += 1 }

(2) Rzuć wyjątek.

object AllDone extends Exception { }
var sum = 0
try {
  for (i <- 0 to 1000) { sum += i; if (sum>=1000) throw AllDone }
} catch {
  case AllDone =>
}

(2a) W Scali 2.8+ jest to już wstępnie spakowane przy scala.util.control.Breaksużyciu składni, która wygląda bardzo podobnie do twojej starej starej przerwy w C / Java:

import scala.util.control.Breaks._
var sum = 0
breakable { for (i <- 0 to 1000) {
  sum += i
  if (sum >= 1000) break
} }

(3) Umieść kod w metodzie i użyj return.

var sum = 0
def findSum { for (i <- 0 to 1000) { sum += i; if (sum>=1000) return } }
findSum

Jest to celowo niezbyt łatwe z co najmniej trzech powodów, o których mogę myśleć. Po pierwsze, w dużych blokach kodu łatwo jest przeoczyć instrukcje „kontynuuj” i „zepsuć” lub myśleć, że wychodzisz z mniej lub bardziej niż naprawdę, lub musisz przerwać dwie pętle, których nie możesz zrobić i tak łatwo - więc standardowe użycie, chociaż przydatne, ma swoje problemy, dlatego powinieneś spróbować ustrukturyzować swój kod w inny sposób. Po drugie, Scala ma różnego rodzaju zagnieżdżenia, których prawdopodobnie nawet nie zauważasz, więc jeśli mógłbyś się wyrwać, prawdopodobnie byłbyś zaskoczony, gdzie skończył się przepływ kodu (szczególnie z zamknięciami). Po trzecie, większość „pętli” Scali nie jest tak naprawdę normalnymi pętlami - to wywołania metod, które mają swoją własną pętlę,Pętla, trudno jest znaleźć spójny sposób, aby wiedzieć, co powinno zrobić „zerwanie” i tym podobne. Aby być konsekwentnym, mądrzejszym rozwiązaniem jest wcale nie mieć „przerwy”.

Uwaga : Istnieją funkcjonalne odpowiedniki wszystkich tych funkcji, w których zwracana jest wartość sumzamiast mutowania jej w miejscu. Są to bardziej idiomatyczne Scala. Jednak logika pozostaje taka sama. ( returnstaje się return xitp.).


9
Jeśli chodzi o wyjątki, chociaż jest to absolutnie prawda, że ​​można zgłosić wyjątek, jest to prawdopodobnie nadużycie mechanizmu wyjątku (patrz Efektywna Java). Wyjątki mają naprawdę wskazywać sytuacje, które są naprawdę nieoczekiwane i / lub wymagają drastycznej ucieczki od kodu, tj. Pewnego rodzaju błędy. Poza tym z pewnością były one dość powolne (nie jestem pewien obecnej sytuacji), ponieważ JVM nie ma powodu, aby je optymalizować.
Jonathan

28
@Jonathan - Wyjątki są powolne tylko wtedy, gdy trzeba obliczyć ślad stosu - zwróć uwagę, jak utworzyłem wyjątek statyczny, aby go wygenerować zamiast generować go w locie! I są to doskonale poprawne konstrukcje kontrolne; są używane w wielu miejscach w bibliotece Scali, ponieważ są naprawdę jedynym sposobem na powrót za pomocą wielu metod (co, jeśli masz stos zamknięć, jest czymś, co czasem trzeba zrobić).
Rex Kerr

18
@ Rex Kerr, zwracasz uwagę na słabości konstrukcji break (nie zgadzam się z nimi), ale sugerujesz stosowanie wyjątków dla normalnego przepływu pracy! Wyjście z pętli nie jest przypadkiem wyjątkowym, jest częścią algorytmu, nie jest tak w przypadku zapisu do nieistniejącego pliku (na przykład). Krótko mówiąc, sugerowane „wyleczenie” jest gorsze niż sama „choroba”. A kiedy zastanawiam się nad rzuceniem prawdziwego wyjątku w breakabledziale ... i wszystkich tych obręczach, żeby uniknąć zła break, hmm ;-) Musisz przyznać, że życie jest ironiczne.
greenoldman

17
@macias - Przepraszam, mój błąd. JVM używa Throwables do sterowania przepływem. Lepszy? To, że zwykle są używane do obsługi obsługi wyjątków, nie oznacza, że ​​można ich używać tylko do obsługi wyjątków. Powrót do określonej lokalizacji z zamknięcia jest jak zgłoszenie wyjątku pod względem kontroli. Nic więc dziwnego, że jest to używany mechanizm.
Rex Kerr

14
@ RexKerr Cóż, za co mnie przekonałeś. Zwykle jestem zwolennikiem wyjątków dla normalnego przebiegu programu, ale dwa główne powody nie mają tutaj zastosowania. Są to: (1) są powolne [nie są używane w ten sposób] oraz (2) sugerują wyjątkowe zachowanie komuś, kto czyta twój kod [nie, jeśli twoja biblioteka pozwala ci je wywoływać break] Jeśli wygląda jak a breaki działa jak break, jeśli o mnie chodzi, to break.
Tim Goodman

66

Zmieniło się to w Scali 2.8, która ma mechanizm korzystania z przerw. Możesz teraz wykonać następujące czynności:

import scala.util.control.Breaks._
var largest = 0
// pass a function to the breakable method
breakable { 
    for (i<-999 to 1  by -1; j <- i to 1 by -1) {
        val product = i * j
        if (largest > product) {
            break  // BREAK!!
        }
        else if (product.toString.equals(product.toString.reverse)) {
            largest = largest max product
        }
    }
}

3
Czy używa to wyjątków pod maską?
Mike

Wykorzystuje to Scalę jako język proceduralny, ignorując zalety programowania funkcjonalnego (tj. Rekurencję ogona). Nie ładna
Galder Zamarreño,

32
Mike: Tak, Scala zgłasza wyjątek, aby wyjść z pętli. Galder: To odpowiada na zadane pytanie „Jak wyjść z pętli w Scali?”. To, czy jest „ładna”, czy nie, nie ma znaczenia.
hohonuuli

2
@hohonuuli, więc jest w bloku try-catch, który się nie złamie, prawda?
greenoldman

2
@Galder Zamarreño Dlaczego rekurencja ogona jest zaletą w tym przypadku? Czy nie jest to po prostu optymalizacja (czyja aplikacja jest ukryta dla nowicjusza i myląco stosowana dla doświadczonych). Czy w tym przykładzie są jakieś korzyści z rekurencji ogona?
user48956,

32

Nigdy nie jest dobrym pomysłem wyjście z pętli for. Jeśli używasz pętli for, oznacza to, że wiesz, ile razy chcesz iterować. Użyj pętli while z 2 warunkami.

na przykład

var done = false
while (i <= length && !done) {
  if (sum > 1000) {
     done = true
  }
}

2
To właśnie uważam za właściwy sposób na wyrwanie się z pętli w Scali. Czy coś jest nie tak z tą odpowiedzią? (biorąc pod uwagę małą liczbę głosów pozytywnych).
Jus12

1
rzeczywiście proste i bardziej czytelne. nawet breakable - break rzecz jest poprawna, wygląda brzydko i ma problemy z wewnętrznym try-catch. Chociaż twoje rozwiązanie nie działa z foreach, głosuję za tobą, szanując prostotę.
yerlilbilgin

13

Aby dodać Rex Kerr, odpowiedz w inny sposób:

  • (1c) Możesz również użyć osłony w swojej pętli:

     var sum = 0
     for (i <- 0 to 1000 ; if sum<1000) sum += i

30
Nie uwzględniłem tego jako opcji, ponieważ tak naprawdę nie przerywa pętli - przebiega przez nią wszystko, ale instrukcja if kończy się niepowodzeniem przy każdej iteracji po tym, jak suma jest wystarczająco wysoka, więc robi tylko jedną instrukcję if wartość pracy za każdym razem. Niestety, w zależności od tego, jak napisałeś pętlę, może to być dużo pracy.
Rex Kerr

@RexKerr: Czy kompilator i tak by go nie zoptymalizował? Czy nie zostanie zoptymalizowany, jeśli nie podczas pierwszego uruchomienia, a następnie podczas JIT?
Maciej Piechotka

5
@MaciejPiechotka - Kompilator JIT ogólnie nie zawiera wystarczająco wyrafinowanej logiki, aby rozpoznać, że instrukcja if na zmieniającej się zmiennej zawsze (w tej szczególnej sytuacji) zwróci wartość false, a zatem może zostać pominięta.
Rex Kerr

6

Ponieważ nie ma breakjeszcze Scali, możesz spróbować rozwiązać ten problem za pomocą return-statement. Dlatego musisz włączyć wewnętrzną pętlę do funkcji, w przeciwnym razie powrót pomija całą pętlę.

Scala 2.8 zawiera jednak sposób na złamanie

http://www.scala-lang.org/api/rc/scala/util/control/Breaks.html


przepraszam, ale chciałem tylko przełamać wewnętrzną pętlę. Nie sugerujesz, że powinienem włączyć tę funkcję?
TiansHUo

Przepraszam, powinienem to wyjaśnić. Oczywiście użycie znaku return oznacza, że ​​musisz zaliczyć pętlę do funkcji. Zredagowałem swoją odpowiedź.
Ham Vocke

1
To wcale nie jest miłe. Wygląda na to, że Scala nie lubi zagnieżdżonych pętli.
TiansHUo

Wydaje się, że nie ma innej drogi. Możesz rzucić
Ham Vocke

4
@TiansHUo: Dlaczego mówisz, że Scala nie lubi zagnieżdżonych pętli? Te same problemy występują, jeśli próbujesz wyjść z jednej pętli.
Rex Kerr


5

Wystarczy użyć pętli while:

var (i, sum) = (0, 0)
while (sum < 1000) {
  sum += i
  i += 1
}

5

Podejście, które generuje wartości w całym zakresie podczas iteracji, aż do warunku zerwania, zamiast generowania najpierw całego zakresu, a następnie iteracji nad nim przy użyciu Iterator(zainspirowany użyciem @RexKerr Stream)

var sum = 0
for ( i <- Iterator.from(1).takeWhile( _ => sum < 1000) ) sum += i

tak lubię to. bez łamliwego usprawiedliwienia, myślę, że wygląda to ładniej.
ses

4

Oto wersja rekurencyjna. W porównaniu ze zrozumieniem jest to nieco tajemnicze, co prawda, ale powiedziałbym, że jest funkcjonalne :)

def run(start:Int) = {
  @tailrec
  def tr(i:Int, largest:Int):Int = tr1(i, i, largest) match {
    case x if i > 1 => tr(i-1, x)
    case _ => largest
  }

  @tailrec
  def tr1(i:Int,j:Int, largest:Int):Int = i*j match {
    case x if x < largest || j < 2 => largest
    case x if x.toString.equals(x.toString.reverse) => tr1(i, j-1, x)
    case _ => tr1(i, j-1, largest)
  }

  tr(start, 0)
}

Jak widać, funkcja tr jest odpowiednikiem zewnętrznego zrozumienia, a tr1 wewnętrznego. Nie ma za co, jeśli znasz sposób na optymalizację mojej wersji.


2

Blisko twojego rozwiązania byłoby to:

var largest = 0
for (i <- 999 to 1 by -1;
  j <- i to 1 by -1;
  product = i * j;
  if (largest <= product && product.toString.reverse.equals (product.toString.reverse.reverse)))
    largest = product

println (largest)

J-iteracja jest wykonywana bez nowego zakresu, a generowanie produktu i warunek są wykonywane w instrukcji for (nie jest to dobre wyrażenie - nie znajduję lepszego). Warunek jest odwrócony, co jest dość szybkie jak na ten problem - może zyskujesz coś z przerwą na większe pętle.

String.reverse domyślnie konwertuje na RichString, dlatego robię 2 dodatkowe odwrotności. :) Bardziej matematyczne podejście może być bardziej eleganckie.


2

breakablePakiet innej firmy jest jedną z możliwych opcji

https://github.com/erikerlandson/breakable

Przykładowy kod:

scala> import com.manyangled.breakable._
import com.manyangled.breakable._

scala> val bkb2 = for {
     |   (x, xLab) <- Stream.from(0).breakable   // create breakable sequence with a method
     |   (y, yLab) <- breakable(Stream.from(0))  // create with a function
     |   if (x % 2 == 1) continue(xLab)          // continue to next in outer "x" loop
     |   if (y % 2 == 0) continue(yLab)          // continue to next in inner "y" loop
     |   if (x > 10) break(xLab)                 // break the outer "x" loop
     |   if (y > x) break(yLab)                  // break the inner "y" loop
     | } yield (x, y)
bkb2: com.manyangled.breakable.Breakable[(Int, Int)] = com.manyangled.breakable.Breakable@34dc53d2

scala> bkb2.toVector
res0: Vector[(Int, Int)] = Vector((2,1), (4,1), (4,3), (6,1), (6,3), (6,5), (8,1), (8,3), (8,5), (8,7), (10,1), (10,3), (10,5), (10,7), (10,9))

2
import scala.util.control._

object demo_brk_963 
{
   def main(args: Array[String]) 
   {
      var a = 0;
      var b = 0;
      val numList1 = List(1,2,3,4,5,6,7,8,9,10);
      val numList2 = List(11,12,13);

      val outer = new Breaks; //object for break
      val inner = new Breaks; //object for break

      outer.breakable // Outer Block
      {
         for( a <- numList1)
         {
            println( "Value of a: " + a);

            inner.breakable // Inner Block
            {
               for( b <- numList2)
               {
                  println( "Value of b: " + b);

                  if( b == 12 )
                  {
                      println( "break-INNER;");
                       inner.break;
                  }
               }
            } // inner breakable
            if( a == 6 )
            {
                println( "break-OUTER;");
                outer.break;
            }
         }
      } // outer breakable.
   }
}

Podstawowa metoda przerywania pętli za pomocą klasy Breaks. Deklarując, że pętla jest przerywalna.


2

Po prostu możemy to zrobić w scala jest

scala> import util.control.Breaks._

scala> object TestBreak{
       def main(args : Array[String]){
       breakable {
       for (i <- 1 to 10){
       println(i)
       if (i == 5){
       break;
       } } } } }

wynik :

scala> TestBreak.main(Array())
1
2
3
4
5

1

Jak na ironię włamanie do Scali scala.util.control.Breaksjest wyjątkiem:

def break(): Nothing = { throw breakException }

Najlepsza rada: NIE używaj break, kontynuuj i gotowe! IMO to ta sama, zła praktyka i złe źródło wszelkiego rodzaju problemów (i gorących dyskusji) i wreszcie „uważane za szkodliwe”. Struktura bloku kodu, również w tym przykładzie przerwy są zbędne. Nasz Edsger W. Dijkstra † napisał:

Jakość programistów jest malejącą funkcją gęstości przejść do instrukcji w programach, które produkują.


1

Mam sytuację taką jak poniższy kod

 for(id<-0 to 99) {
    try {
      var symbol = ctx.read("$.stocks[" + id + "].symbol").toString
      var name = ctx.read("$.stocks[" + id + "].name").toString
      stocklist(symbol) = name
    }catch {
      case ex: com.jayway.jsonpath.PathNotFoundException=>{break}
    }
  }

Używam biblioteki Java, a mechanizm polega na tym, że ctx.read zgłasza wyjątek, gdy nic nie może znaleźć. Byłem uwięziony w sytuacji, w której: musiałem przerwać pętlę, gdy został zgłoszony wyjątek, ale scala.util.control.Breaks.break użył wyjątku, aby przerwać pętlę, i był on w bloku catch, więc został złapany.

Mam brzydki sposób na rozwiązanie tego problemu: wykonaj pętlę po raz pierwszy i oblicz rzeczywistą długość. i użyj go do drugiej pętli.

wziąć przerwę od Scali nie jest tak dobre, gdy używasz niektórych bibliotek Java.


1

Jestem nowy w Scali, ale co powiesz na to, aby uniknąć zgłaszania wyjątków i powtarzania metod:

object awhile {
def apply(condition: () => Boolean, action: () => breakwhen): Unit = {
    while (condition()) {
        action() match {
            case breakwhen(true)    => return ;
            case _                  => { };
        }
    }
}
case class breakwhen(break:Boolean);

użyj tego w ten sposób:

var i = 0
awhile(() => i < 20, () => {
    i = i + 1
    breakwhen(i == 5)
});
println(i)

jeśli nie chcesz się zepsuć:

awhile(() => i < 20, () => {
    i = i + 1
    breakwhen(false)
});

1

Sprytne użycie findmetody zbierania załatwi sprawę.

var largest = 0
lazy val ij =
  for (i <- 999 to 1 by -1; j <- i to 1 by -1) yield (i, j)

val largest_ij = ij.find { case(i,j) =>
  val product = i * j
  if (product.toString == product.toString.reverse)
    largest = largest max product
  largest > product
}

println(largest_ij.get)
println(largest)

1

Poniżej znajduje się kod do przerwania pętli w prosty sposób

import scala.util.control.Breaks.break

object RecurringCharacter {
  def main(args: Array[String]) {
    val str = "nileshshinde";

    for (i <- 0 to str.length() - 1) {
      for (j <- i + 1 to str.length() - 1) {

        if (str(i) == str(j)) {
          println("First Repeted Character " + str(i))
          break()     //break method will exit the loop with an Exception "Exception in thread "main" scala.util.control.BreakControl"

        }
      }
    }
  }
}

1

Nie wiem, jak bardzo zmienił się styl Scali w ciągu ostatnich 9 lat, ale uznałem za interesujące, że większość istniejących odpowiedzi używa varslub trudna do odczytania rekurencja. Kluczem do wcześniejszego wyjścia jest użycie leniwej kolekcji do wygenerowania potencjalnych kandydatów, a następnie osobne sprawdzenie stanu. Aby wygenerować produkty:

val products = for {
  i <- (999 to 1 by -1).view
  j <- (i to 1 by -1).view
} yield (i*j)

Następnie, aby znaleźć pierwszy palindrom z tego widoku bez generowania każdej kombinacji:

val palindromes = products filter {p => p.toString == p.toString.reverse}
palindromes.head

Aby znaleźć największy palindrom (chociaż lenistwo nie kupuje zbyt wiele, ponieważ i tak musisz sprawdzić całą listę):

palindromes.max

Twój oryginalny kod w rzeczywistości sprawdza pierwszy palindrom, który jest większy niż kolejny produkt, co jest tym samym, co sprawdzanie pierwszego palindromu, z wyjątkiem dziwnych warunków brzegowych, których nie sądzę, że zamierzałeś. Produkty nie zmniejszają się ściśle monotonicznie. Na przykład 998*998jest większy niż 999*997, ale pojawia się znacznie później w pętlach.

W każdym razie zaletą oddzielnego leniwego generowania i sprawdzania stanu jest to, że piszesz je tak, jakby używało całej listy, ale generuje tylko tyle, ile potrzebujesz. Dostajesz to, co najlepsze z obu światów.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.