2016-04-09 5 views
11

Titel sagt alles, wirklich; Das Iterieren über die Sammlung, während der Status zwischen den Schleifen beibehalten wird, und das Beenden der Iteration basierend auf der Beendigungsbedingung sowie das einfache Auslaufen von Elementen kann das gebräuchlichste Muster sein, um irgendetwas in der imperativen Programmierung zu erreichen. Es scheint mir jedoch, wie es etwas funktionelle gentleprogrammers ist vereinbart über oder zumindest zu sprechen Ich traf nie ein Idiom für sie oder ein halb-genormte Namen wie mit map, fold, reduce usw.Gibt es in der funktionalen Programmierung ein Konzept für 'falten mit Pause' oder 'mit Akku finden'?

ich oft verwenden die followinig Code in scala:

implicit class FoldWhile[T](private val items :Iterable[T]) extends AnyVal { 
    def foldWhile[A](start :A)(until :A=>Boolean)(op :(A, T)=>A) :A = { 
     if (until(start)) start 
     else { 
      var accumulator = start 
      items.find{ e => accumulator = op(accumulator, e); until(accumulator) } 
      accumulator 
     } 

    } 

} 

Aber es ist hässlich. Jedes Mal, wenn ich einen deklarativen Ansatz versuche, komme ich mit noch mehr und fast sicher langsamem Code, ähnlich:

Iterator.iterate((start, items.iterator)){ 
    case (acc, i) if until(acc) => (acc, i) 
    case (acc, i) if i.hasNext => (op(acc, i.next()), i) 
    case x => x 
}.dropWhile { 
    case (acc, i) => !until(acc) && i.hasNext 
}.next()._1 

(Eine funktionelle Variante List s oder Stream s verwenden würde, aber Iteratoren haben wohl weniger Overhead als die Umwandlung items zu einem Stream, als Standard-Implementierung für letztere verwendet sowieso einen Iterator darunter).

Meine Fragen sind:

1) Ist dieses Konzept einen Namen in der funktionalen Programmierung hat, und wenn ja, was ist das Muster mit der Umsetzung verbunden?

2) Was wäre der beste (d. H. Prägnante, generische, faule und am wenigsten Overhead) Weg, es in scala zu implementieren?

+1

Ich habe nie verstanden, warum dies keine Standardimplementierung hat. Ja. Tail Rekursion ist der Weg, es zu tun, aber es ist ein bisschen hässlich (und erfordert eine Hilfsfunktion, für die man einen Namen finden muss, der für mich immer ein bisschen wie ein Code-Geruch aussieht). .'mapUntil' und 'foldLeftUntil' usw. scheinen mir nützliche Dinge zu sein ... –

Antwort

9

Dies ist verpönt von scala-Puristen, aber Sie können eine return Anweisung wie folgt verwendet werden:

def foldWhile[A](zero: A)(until:A => Boolean)(op: (A,T) => A): A = items.fold(zero) { 
     case (a, b) if until(a) => return a 
     case (a,b) => op(a, b) 
} 

Oder, wenn Sie einer von denen sind Stirnrunzeln, und eine rein funktionale Lösung ohne schmutzige Imperativ Tricks möchte Sie können etwas faul, wie ein Iterator oder einen Stream verwenden:

items 
    .toStream // or .iterator - it doesn't really matter much in this case 
    .scanLeft(zero)(op) 
    .find(until) 
+1

Haha, ich habe vergessen, scala hat eine return-Anweisung :) Ich denke, dass in einer zweizeiligen Methode es entschuldbar und tatsächlich sowohl effizienter als auch lesbar ist als mit einer var. Um einen Kommentar aus der Quelle einer der Scala-Sammlungen zu zitieren, neben einem Niedergeschlagenen: "Es ist eine unvollkommene Welt, aber zumindest können wir die Unvollkommenheit ausgleichen". Und auch der eine Liner mit Scan ist sicher eine Erinnerung wert - danke! – Turin

4

Die funktionale Möglichkeit, solche Dinge zu tun, ist über Tail Recursion:

implicit class FoldWhile[T](val items: Iterable[T]) extends AnyVal { 

    def foldWhile[A](zero: A)(until: A => Boolean)(op: (A, T) => A): A = { 
    @tailrec def loop(acc: A, remaining: Iterable[T]): A = 
     if (remaining.isEmpty || !until(acc)) acc else loop(op(acc, remaining.head), remaining.tail) 

    loop(zero, items) 
    } 
} 

Rekursion Verwenden Sie bei jedem Schritt entscheiden können, ob Sie break ohne Verwendung und ohne Overhead, weil Schwanz Rekursion gehen oder nicht wollen werden vom Compiler in Iterationen konvertiert.

Auch Mustererkennung wird oft verwendet, um Sequenzen zu zerlegen. Zum Beispiel könnten, wenn Sie ein List haben Sie tun:

implicit class FoldWhile[T](val items: List[T]) extends AnyVal { 

    def foldWhile[A](zero: A)(until: A => Boolean)(op: (A, T) => A): A = { 
    @tailrec def loop(acc: A, remaining: List[T]): A = remaining match { 
     case Nil    => acc 
     case _ if !until(acc) => acc 
     case h :: t   => loop(op(acc, h), t) 
    } 

    loop(zero, items) 
    } 
} 

Scala hat die @scala.annotation.tailrec Anmerkung zu Kraft Kompilierung fehlschlagen, wenn die Funktion, die Sie mit Anmerkungen versehen nicht Schwanz ist rekursiv. Ich schlage vor, dass Sie es so oft wie möglich verwenden, da es sowohl Fehler vermeidet als auch den Code dokumentiert.

+0

In rein funktionaler Sprache wäre das wohl die praktischste Antwort. Ich habe die Rekursion jedoch hier nicht im Sinn, da 'tail' mit einigen Sammlungen eine sehr langsame Operation sein kann (sogar mit Vector ist es weit vom Ideal entfernt) und ich glaube (vielleicht zu Unrecht), dass im Allgemeinen die bevorzugte Art, Iterationen durchzuführen Nicht-konkreter Sammlungs-Typ verwendet eingebaute Methoden; Jede externe Iteration, einschließlich Rekursion, muss eine zusätzliche Iterator/Stream-Konvertierungsschicht durchlaufen und bei jedem Methodenaufruf validiert werden, der beim Ausführen eines Callbacks in der Regel ignoriert wird. – Turin

+1

Und ehrlich gesagt habe ich heimlich die Hoffnung gehegt, einen Begriff aus der Kategorientheorie zu hören, der neue Horizonte eröffnen würde (wie Monoids und reduzieren);) – Turin

+0

@Turin Sie sagen, dass die spezifische Implementierung der Rekursion von der Sammlung abhängt. Die eine, die ich Ihnen gezeigt habe, ist für eine listenähnliche Sammlung mit schneller Dekomposition von Kopf und Schwanz. Aber im Allgemeinen ist Rekursion das, was Sie entscheiden lässt, wenn Sie bei jedem Schritt anhalten wollen, wie Sie den Schritt implementieren, liegt bei Ihnen (z. B. Head-Tail-Dekomposition, ein veränderbarer Iterator, was auch immer). Ich werde auch auf ein "reduziertes Monoid" warten, ich bin definitiv kein hardcore Funktionsprogrammierer: D –

1

eine rechte Falte, wenn träge gemacht, kann die vorzeitige Beendigung tun.In Haskell, zum Beispiel, können Sie die find Funktion (Return erste Element der Liste, die Prädikat erfüllt) schreiben mit foldr:

find :: (a -> Bool) -> [a] -> Maybe a 
find p = foldr (\a r -> if p a then Just a else r) Nothing 

-- For reference: 
foldr :: (a -> r -> r) -> r -> [a] -> r 
foldr _ z [] = [] 
foldr f z (a:as) = f a (foldr f z as) 

Was passiert, wenn Sie versuchen, sagen wir, find even [1..]? (Beachten Sie, dass dies eine unendliche Liste!)

find even [1..] 
    = foldr (\a r -> if even a then Just a else r) Nothing [1..] 
    = if even 1 
    then Just 1 
    else foldr (\a r -> if even a then Just a else r) Nothing ([2..]) 
    = if False 
    then Just 1 
    else foldr (\a r -> if even a then Just a else r) Nothing ([2..]) 
    = foldr (\a r -> if even a then Just a else r) Nothing ([2..]) 
    = if even 2 
    then Just 2 
    else foldr (\a r -> if even a then Just a else r) Nothing ([3..]) 
    = if True 
    then Just 2 
    else foldr (\a r -> if even a then Just a else r) Nothing ([3..]) 
    = Just 2 

Laziness bedeutet, dass die Funktion, die wir mit (\a r -> if even a then Just a else r) falten zu entscheiden, ob bekommt das r Argument das eine zwingen, deren Auswertung erfordert, dass wir die Liste rekursiv -überhaupt. Wenn also even 2 zu True ausgewertet wird, wählen wir den Zweig if ... then ... else ... aus, der das Ergebnis, das vom Ende der Liste berechnet wurde, verwirft - was bedeutet, dass wir es nie auswerten. (Es läuft auch in konstantem Raum. Während Programmierer in eifrigen funktionalen Sprachen lernen, foldr aufgrund von Platz- und Terminierungsproblemen zu vermeiden, sind diese in faulen Sprachen nicht immer zutreffend!)

Dies hängt natürlich davon ab, dass Haskell wird faul bewertet, aber es sollte trotzdem möglich sein, dies in einer eifrigen Sprache wie Scala zu simulieren - ich weiß, dass es eine lazy val Funktion hat, die dafür geeignet sein könnte. Es sieht so aus, als müssten Sie eine lazyFold-Funktion schreiben, die nach rechts klappt, aber die Rekursion findet innerhalb eines Lazy-Wertes statt. Möglicherweise haben Sie jedoch weiterhin Probleme mit der Speicherplatznutzung.

+0

Danke, es öffnete mir die Augen für die Nützlichkeit von foldr; out of the box ist es viel weniger nützlich, als in scala foldl, ohne Faulheit von haskell, aber mit Stackoverlow Bombe xoming. Obwohl es sicherlich hässlicher als in Haskell sein wird, könnte es sicherlich in Scala simuliert werden. – Turin

+0

Ok, bin am Morgen zurück und es scheint mir, dass es nicht mein erstes Skript tut, da das Stop-Prädikat ein Element der Sammlung und nicht den Akkumulator akzeptiert. Ich glaube nicht, dass es einen Weg gibt, einen Kuchen zu essen und ihn mit foldr zu essen, d. H.beide haben den Wert eines Akkumulators bei irgendeinem Element und vermeiden es, es für den Rest auszuwerten. Wenn wir den Stack hinuntergehen, haben wir keine Informationen über das Ergebnis des Akkumulators, und zurück sind wir schon durch die gesamte Sammlung gegangen und müssen sowieso durch den ganzen Stapel laufen. – Turin

Verwandte Themen