web-dev-qa-db-ger.com

Funktionale Programmierung, Scala map and fold left

Welche guten Tutorials gibt es noch?

rsprüngliche Frage, aus dem Löschvorgang wiederhergestellt, um den Kontext für andere Antworten bereitzustellen:

Ich versuche, eine Methode zu implementieren, um die Boudning-Box aus Rechteck, Kreis, Position und der Gruppe zu finden, die Shape erweitert. Gruppe ist im Grunde eine Reihe von Formen

abstract class Shape  
case class Rectangle(width: Int, height: Int) extends Shape  
case class Location(x: Int, y: Int, shape: Shape) extends Shape  
case class Circle(radius: Int) extends Shape  
case class Group(shape: Shape*) extends Shape  

Ich habe den Begrenzungsrahmen für alle drei außer für die erste Gruppe berechnen lassen. Jetzt weiß ich, dass ich für die Bounding-Box-Methode Map and Fold Left für Group verwenden sollte, aber ich kann die genaue Syntax für die Erstellung einfach nicht herausfinden.

object BoundingBox {  
  def boundingBox(s: Shape): Location = s match {  
    case Circle(c)=>   
      new Location(-c,-c,s)  
    case Rectangle(_, _) =>  
      new Location(0, 0, s)  
    case Location(x, y, shape) => {  
      val b = boundingBox(shape)  
      Location(x + b.x, y + b.y, b.shape)  
    }  
    case Group(shapes @ _*) =>  ( /: shapes) { } // i dont know how to proceed here.
  }
}

Gruppenbegrenzungsrahmen ist im Grunde der kleinste Begrenzungsrahmen mit allen eingeschlossenen Formen.

53
jon

Nachdem Sie eine fast völlig andere Frage gestellt haben, gebe ich eine andere Antwort. Anstatt auf ein Tutorial zu Karten und Falzen zu verweisen, gebe ich nur eines.

In Scala müssen Sie zunächst wissen, wie Sie eine anonyme Funktion erstellen. Es geht so, von allgemein bis spezifisch:

(var1: Type1, var2: Type2, ..., varN: TypeN) => /* output */
(var1, var2, ..., varN) => /* output, if types can be inferred */
var1 => /* output, if type can be inferred and N=1 */

Hier sind einige Beispiele:

(x: Double, y: Double, z: Double) => Math.sqrt(x*x + y*y + z*z)
val f:(Double,Double)=>Double = (x,y) => x*y + Math.exp(-x*y)
val neg:Double=>Double = x => -x

Die map -Methode von Listen und dergleichen wendet nun eine Funktion (anonym oder anderweitig) auf jedes Element der Karte an. Das heißt, wenn Sie haben

List(a1,a2,...,aN)
f:A => B

dann

List(a1,a2,...,aN) map (f)

produziert

List( f(a1) , f(a2) , ..., f(aN) )

Es gibt viele Gründe, warum dies nützlich sein kann. Vielleicht haben Sie ein paar Zeichenfolgen und möchten wissen, wie lang jede ist, oder Sie möchten, dass alle in Großbuchstaben geschrieben werden, oder Sie möchten, dass sie rückwärts geschrieben werden. Wenn Sie eine Funktion haben, die das tut, was Sie wollen one Element, wird map dies für alle Elemente tun:

scala> List("How","long","are","we?") map (s => s.length)
res0: List[Int] = List(3, 4, 3, 3)

scala> List("How","capitalized","are","we?") map (s => s.toUpperCase)
res1: List[Java.lang.String] = List(HOW, CAPITALIZED, ARE, WE?)

scala> List("How","backwards","are","we?") map (s => s.reverse)
res2: List[scala.runtime.RichString] = List(woH, sdrawkcab, era, ?ew)

Das ist also eine Karte im Allgemeinen und in der Scala.

Was aber, wenn wir unsere Ergebnisse sammeln wollen? Hier kommt fold ins Spiel (foldLeft ist die Version, die links beginnt und rechts funktioniert).

Nehmen wir an, wir haben eine Funktion f:(B,A) => B, das heißt, wir brauchen ein B und ein A und kombinieren sie, um ein B zu erzeugen. Nun, wir könnten mit einem B beginnen und dann unsere Liste von A's hinein geben eine nach der anderen, und am Ende hätten wir eine B. Genau das macht fold. foldLeft beginnt am linken Ende der Liste; foldRight beginnt von rechts. Das ist,

List(a1,a2,...,aN) foldLeft(b0)(f)

produziert

f( f( ... f( f(b0,a1) , a2 ) ... ), aN )

dabei ist b0 natürlich Ihr Anfangswert.

Vielleicht haben wir also eine Funktion, die ein int und einen string nimmt und das int oder die Länge des strings zurückgibt, je nachdem, welcher Wert größer ist. Wenn wir unsere liste damit gefaltet hätten, würde dies uns den längsten string mitteilen (vorausgesetzt wir Beginnen Sie mit 0). Oder wir können die Länge zum int addieren und dabei Werte akkumulieren.

Lass es uns versuchen.

scala> List("How","long","is","longest?").foldLeft(0)((i,s) => i max s.length) 
res3: Int = 8

scala> List("How","long","is","everyone?").foldLeft(0)((i,s) => i + s.length)
res4: Int = 18

Okay, gut, aber was ist, wenn wir wissen wollen, wer ist die längste? Eine Möglichkeit (vielleicht nicht die beste, aber sie zeigt ein nützliches Muster) besteht darin, sowohl die Länge (eine Ganzzahl) als auch des Hauptkonkurrenten (eine Zeichenfolge) mitzuführen. Lass es uns versuchen:

scala> List("Who","is","longest?").foldLeft((0,""))((i,s) => 
     |   if (i._1 < s.length) (s.length,s)
     |   else i
     | )
res5: (Int, Java.lang.String) = (8,longest?)

Hier ist i jetzt ein Tupel vom Typ (Int,String), Und i._1 Ist der erste Teil dieses Tupels (ein Int).

Aber in einigen Fällen wie diesen wollen wir nicht wirklich, dass wir eine Falte verwenden. Wenn wir den längeren von zwei Strings wollen, wäre die natürlichste Funktion eine wie max:(String,String)=>String. Wie wenden wir das an?

Nun, in diesem Fall gibt es einen "kürzesten" Standardfall, sodass wir die String-Max-Funktion ab "" falten können. Aber ein besserer Weg ist Verkleinern. Wie bei fold gibt es zwei Versionen, eine von links und eine von rechts. Es nimmt keinen Anfangswert an und erfordert eine Funktion f:(A,A)=>A. Das heißt, es werden zwei Dinge benötigt und es wird eine vom selben Typ zurückgegeben. Hier ist ein Beispiel mit einer String-Max-Funktion:

scala> List("Who","is","longest?").reduceLeft((s1,s2) =>              
     |   if (s2.length > s1.length) s2
     |   else s1
     | )
res6: Java.lang.String = longest?

Jetzt gibt es nur noch zwei Tricks. Erstens bedeuten die folgenden beiden dasselbe:

list.foldLeft(b0)(f)
(b0 /: list)(f)

Beachten Sie, dass die Sekunde kürzer ist und Sie den Eindruck haben, dass Sie b0 Nehmen und etwas mit der Liste (die Sie sind) anfangen. (:\ Ist dasselbe wie foldRight, aber Sie verwenden es so: (list :\ b0) (f)

Zweitens können Sie, wenn Sie nur einmal auf eine Variable verweisen, _ Anstelle des Variablennamens verwenden und den Teil x => Der anonymen Funktionsdeklaration weglassen. Hier sind zwei Beispiele:

scala> List("How","long","are","we?") map (_.length)
res7: List[Int] = List(3, 4, 3, 3)

scala> (0 /: List("How","long","are","we","all?"))(_ + _.length)
res8: Int = 16

Zu diesem Zeitpunkt sollten Sie in der Lage sein, Funktionen mit Scala zu erstellen und zuzuordnen, zu falten und zu verkleinern. Wenn Sie also wissen, wie Ihr Algorithmus funktionieren soll, sollte es ziemlich einfach sein, ihn zu implementieren.

262
Rex Kerr

Der grundlegende Algorithmus würde so aussehen:

shapes.tail.foldLeft(boundingBox(shapes.head)) {
  case (box, shape) if box contains shape => box
  case (box, shape) if shape contains box => shape
  case (box, shape) => boxBounding(box, shape)
}

Jetzt müssen Sie contains und boxBounding schreiben, was mehr ein reines Algorithmusproblem als ein Sprachproblem ist.

Wenn alle Formen den gleichen Mittelpunkt hätten, wäre die Implementierung von contains einfacher. Es würde so gehen:

abstract class Shape { def contains(s: Shape): Boolean }
case class Rectangle(width: Int, height: Int) extends Shape {
  def contains(s: Shape): Boolean = s match {
    case Rectangle(w2, h2) => width >= w2 && height >= h2
    case Location(x, y, s) => // not the same center
    case Circle(radius) => width >= radius && height >= radius
    case Group(shapes @ _*) => shapes.forall(this.contains(_))
  }
}
case class Location(x: Int, y: Int, shape: Shape) extends Shape {
  def contains(s: Shape): Boolean = // not the same center
}
case class Circle(radius: Int) extends Shape {
  def contains(s: Shape): Boolean = s match {
    case Rectangle(width, height) => radius >= width && radius >= height
    case Location(x, y) => // not the same center
    case Circle(r2) => radius >= r2
    case Group(shapes @ _*) => shapes.forall(this.contains(_))
  }
}
case class Group(shapes: Shape*) extends Shape {
  def contains(s: Shape): Boolean = shapes.exists(_ contains s)
}

boxBounding, das zwei Formen annimmt und kombiniert, ist normalerweise ein Rechteck, kann aber unter bestimmten Umständen ein Kreis sein. Wie auch immer, es ist ziemlich einfach, sobald Sie den Algorithmus herausgefunden haben.

4

Ein Begrenzungsrahmen ist normalerweise ein Rechteck. Ich glaube nicht, dass ein Kreis bei (-r, -r) der Begrenzungsrahmen eines Kreises mit dem Radius r ist.

Angenommen, Sie haben einen Begrenzungsrahmen b1 und einen anderen b2 und eine Funktion combineBoxes, die den Begrenzungsrahmen von b1 und b2 berechnet.

Wenn Sie dann eine nicht leere Gruppe von Formen in Ihrer Gruppe haben, können Sie reduceLeft verwenden, um den gesamten Begrenzungsrahmen einer Liste von Begrenzungsrahmen zu berechnen, indem Sie beide gleichzeitig kombinieren bis nur noch eine riesige Kiste übrig ist. (Dieselbe Idee kann verwendet werden, um eine Liste von Zahlen durch paarweises Hinzufügen zu einer Summe von Zahlen zu reduzieren. Sie heißt reduceLeft, da sie in der Liste von links nach rechts funktioniert.)

Angenommen, blist ist eine Liste von Begrenzungsrahmen für jede Form. (Hinweis: Hier kommt map ins Spiel.) Dann

val bigBox = blist reduceLeft( (box1,box2) => combineBoxes(box1,box2) )

Sie müssen den leeren Gruppenfall jedoch separat abfangen. (Da es keinen genau definierten Begrenzungsrahmen gibt, möchten Sie keine Falze verwenden. Falze eignen sich, wenn ein leerer Standardfall sinnvoll ist. Oder Sie müssen mit Option falzen, aber dann muss Ihre Kombinationsfunktion verstehen, wie man None mit Some(box) kombiniert, was sich in diesem Fall wahrscheinlich nicht lohnt - aber sehr gut, wenn Sie Produktionscode schreiben, der dies erfordert Behandle elegant verschiedene Arten von Situationen mit leeren Listen.)

2
Rex Kerr