Sortieren in Scala - ein Beispiel für Katzen

Hallo Habr! Ich bringe eine russische Übersetzung meines Artikels über Medium: Sorting in Scala - ein Beispiel für einen Katzenladen - vor Gericht . Der Artikel richtet sich an Leser, die die Syntax der Scala-Sprache kennen und sich mit den grundlegenden Werkzeugen der Standardbibliothek auskennen.


Trotz der Tatsache, dass sowohl Java als auch Scala die JVM als Laufzeitplattform verwenden, hat Scala dank einer signifikanten Anpassung der Konzepte der funktionalen Programmierung und einer umfangreichen Standardbibliothek als viel ausdrucksstärkere Sprache an Bekanntheit gewonnen. In diesem Artikel werde ich ein Beispiel für diese Ausdruckskraft betrachten und versuchen, mir vorzustellen, wie sich ein kleiner Teil der Codebasis und der entsprechenden Anforderungen im Laufe der Zeit entwickeln kann.


Erste Erklärung des Problems


Stellen Sie sich vor, wir haben einen Katzenladen zur Verfügung (weil Katzen das beliebteste Tier im Scala-Ökosystem sind). Aufgrund der Besonderheiten, Informationen über zum Verkauf angebotene Katzen zu erhalten, stammen die Informationen in einigen Fällen nicht aus der Datenbank und müssen manuell sortiert werden, bevor eine HTTP-Antwort an den Kunden gesendet oder auf andere Weise verarbeitet wird. Das Hauptobjekt des Themenbereichs ist natürlich, Catdass zu Beginn nur drei Felder primitiver Typen vorhanden sind. Ziel ist es, eine API zum Sortieren von Katzensammlungen zu entwickeln, die die folgenden Anforderungen erfüllt:


  • Die Sortierreihenfolge kann für jedes der Felder festgelegt werden.
  • Die Sortierreihenfolge darf für keines der Felder definiert werden
  • Die Sortierung muss für Felder mit einer undefinierten Sortierreihenfolge stabil sein
  • (.. age name)


case class Cat(age: Int,
               name: String,
               available: Boolean)

, , . , scala.Ordering Tuple3, Ordering 3, Cat.


, Tuple3 , . , — Tuple1, Tuple2, Tuple3, Ordering, . 9 (3 3 ), .


, API , " " . , 3 : (), ( ) "" ( ). (ADT):


sealed trait SortOrder

object SortOrder {

  case object Keep extends SortOrder

  case object Asc extends SortOrder

  case object Desc extends SortOrder
}

Ordering. SortOrder , , , :


import common.OrderingUtil
import iteration1.SortOrder.{Asc, Desc, Keep}

object syntax {

  implicit class OrderSyntax(val order: SortOrder) extends AnyVal {

    def apply[A](ordering: Ordering[A]): Ordering[A] =
      order match {
        case Keep => OrderingUtil.identity
        case Asc => ordering
        case Desc => ordering.reverse
      }
  }
}

OrderingUtil.identity — , A, . : Ordering.by(_ => 0).


, , Ordering[Cat]. CatOrdering:


import iteration1.syntax._

object CatOrdering {

  def of(idOrder: SortOrder,
         nameOrder: SortOrder,
         availableOrder: SortOrder): Ordering[Cat] =
    Ordering
      .Tuple3(idOrder(Ordering.Int), nameOrder(Ordering.String), availableOrder(Ordering.Boolean))
      .on[Cat](cat => (cat.age, cat.name, cat.available))
}

(Ordering[Cat]) CatOrdering.of:


CatOrdering.of(SortOrder.Asc, SortOrder.Keep, SortOrder.Desc)

, ScalaTest ScalaCheck property-based . , . .



Cat .


case class Cat(age: Int,
               name: String,
               available: Boolean,
               owner: Option[String])

, — ( null!), , , , . , 4 :


  1. , ()
  2. ,
  3. ,
  4. , ( )

(1 4) scala.Ordering.Option, 2 . SortOrder , :


sealed trait SortOrder

object SortOrder {

  case class Asc(emptyFirst: Boolean) extends SortOrder

  case class Desc(emptyFirst: Boolean) extends SortOrder

  case object Keep extends SortOrder

  object Asc {
    def emptyFirst: Asc = Asc(emptyFirst = true)

    def emptyLast: Asc = Asc(emptyFirst = false)
  }

  object Desc {
    def emptyFirst: Desc = Desc(emptyFirst = true)

    def emptyLast: Desc = Desc(emptyFirst = false)
  }
}

SortOrder. optional Ordering[Option[A]] A, , SortOrder. , , Ordering[Option[A]] apply, Ordering[Option[A]], . , A apply Option. <:<, StackOverflow ( Dotty Not ).


import common.OrderingUtil
import iteration2.sort_order.SortOrder._

object syntax {

  private object OptionOrdering {

    def apply[A](rootOrdering: Ordering[A],
                 emptyFirst: Boolean): Ordering[Option[A]] =
      if (emptyFirst)
        OptionOrdering.emptyFirst(rootOrdering)
      else
        OptionOrdering.emptyLast(rootOrdering)

    def emptyFirst[A](rootOrdering: Ordering[A]): Ordering[Option[A]] =
      (x: Option[A], y: Option[A]) => (x, y) match {
        case (None, None) => 0
        case (None, _) => -1
        case (_, None) => 1
        case (Some(a), Some(b)) => rootOrdering.compare(a, b)
      }

    def emptyLast[A](rootOrdering: Ordering[A]): Ordering[Option[A]] =
      (x: Option[A], y: Option[A]) => (x, y) match {
        case (None, None) => 0
        case (None, _) => 1
        case (_, None) => -1
        case (Some(a), Some(b)) => rootOrdering.compare(a, b)
      }
  }

  implicit class OrderSyntax(val order: SortOrder) extends AnyVal {

    def optional[A](ordering: Ordering[A]): Ordering[Option[A]] =
      order match {
        case Keep => OrderingUtil.identity
        case Asc(emptyFirst) => OptionOrdering(ordering, emptyFirst)
        case Desc(emptyFirst) => OptionOrdering(ordering.reverse, emptyFirst)
      }

    def apply[A](ordering: Ordering[A]): Ordering[A] =
      order match {
        case Keep => OrderingUtil.identity
        case Asc(_) => ordering
        case Desc(_) => ordering.reverse
      }
  }
}

import iteration2.sort_order.SortOrder
import iteration2.sort_order.syntax._

import scala.Ordering.{Boolean => BooleanO, Int => IntO, String => StringO}

object CatOrdering {

  def toOrdering(idOrder: SortOrder,
                 nameOrder: SortOrder,
                 availableOrder: SortOrder,
                 ownerOrder: SortOrder): Ordering[Cat] = {
    Ordering
      .Tuple4(idOrder(IntO), nameOrder(StringO), availableOrder(BooleanO), ownerOrder.optional(StringO))
      .on[Cat](cat => (cat.age, cat.name, cat.available, cat.owner))
  }
}

Ordering[Cat] . , .


CatOrdering.toOrdering(
  SortOrder.Asc.emptyFirst, 
  SortOrder.Asc.emptyFirst, 
  SortOrder.Asc.emptyFirst,
  SortOrder.Asc.emptyFirst
)

, Option, . .



. , :


  1. .
  2. - , SortOrder.Keep.
  3. Cat 9. Tuple10 .

- . , Cat 10 . , , . , , SortOrder . .


, , . Cat. ( №1) ( №2), ( №3). (SortOrder ):


import java.time.LocalDate

case class Cat(age: Int,
               name: String,
               available: Boolean,
               owner: Option[String],
               breed: String,
               furColor: String,
               eyeColor: String,
               registrationId: String,
               lastHealthCheck: Option[LocalDate],
               urgentSell: Boolean)

import java.time.LocalDate

import iteration3.sort_order.SortOrder
import iteration3.sort_order.syntax._

import scala.Ordering._

sealed trait CatField {
  def toOrdering(sortOrder: SortOrder): Ordering[Cat]
}

object CatField {

  case object Age extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.Int).on(_.age)
  }

  case object Name extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.String).on(_.name)
  }

  case object Available extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.Boolean).on(_.available)
  }

  case object Owner extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder.optional(Ordering.String).on(_.owner)
  }

  case object Breed extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.String).on(_.breed)
  }

  case object FurColor extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.String).on(_.furColor)
  }

  case object EyeColor extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.String).on(_.eyeColor)
  }

  case object RegistrationId extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.String).on(_.registrationId)
  }

  case object LastHealthCheck extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder.optional(Ordering.by[LocalDate, Long](_.toEpochDay)).on(_.lastHealthCheck)
  }

  case object UrgentSell extends CatField {
    override def toOrdering(sortOrder: SortOrder): Ordering[Cat] =
      sortOrder(Ordering.Boolean).on(_.urgentSell)
  }
}

import common.OrderingUtil
import iteration3.sort_order.SortOrder

object CatOrdering {

  def byFields(fields: Seq[(CatField, SortOrder)]): Ordering[Cat] =
    if (fields.isEmpty) OrderingUtil.identity[Cat]
    else {
      val (head, headOrder) = fields.head
      val (res, _) = fields.tail.foldLeft[(Ordering[Cat], Set[CatField])]((head.toOrdering(headOrder), Set())) {
        case (acc@(_, presentFields), (field, _)) if presentFields.contains(field) =>
          acc

        case ((ordering, presentFields), (field, order)) =>
          (ordering.orElse(field.toOrdering(order)), presentFields + field)
      }
      res
    }
}

orElse, , . thenComparing Comparator Java. Ordering, Comparator . , orElse Scala, orElseBy.


, , byFields , . , OrderingUtil.identity, . " ", foldLeft, .


SortOrder.Keep, , , . , , . HTTP , .


. , , , . ( ) , , . , , , , , .


.



Obwohl die endgültige Implementierung in anderen Programmiersprachen ähnlich aussehen mag, scheint mir Scala einen guten Kompromiss zwischen Typensicherheit und Verständlichkeit und Lesbarkeit des Codes zu bieten. Ich hoffe, dass diese Veröffentlichung dazu beigetragen hat, das Sortieren von Sammlungen in Scala besser zu verstehen, oder Ideen vorgeschlagen hat, die in Code angewendet werden können, der nicht in direktem Zusammenhang steht Ordering.


Der gesamte Beispielcode ist in diesem Github-Repository verfügbar .


All Articles