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, Cat
dass 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 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
, . .
. , :
- .
- - ,
SortOrder.Keep
. 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 .