Bonjour, Habr! J'apporte à votre tribunal une traduction russe de mon article sur Medium: Sorting in Scala - un exemple de boutique de chats . L'article est destiné aux lecteurs qui connaissent la syntaxe du langage Scala et connaissent les outils de base de la bibliothèque standard.
Malgré le fait que Java et Scala utilisent la JVM comme plate-forme d'exécution, Scala a acquis une renommée en tant que langage beaucoup plus expressif, grâce à une adaptation significative des concepts de programmation fonctionnelle et une riche bibliothèque standard. Dans cet article, je considérerai un exemple de cette expressivité, en essayant d'imaginer comment une petite partie de la base de code et les exigences correspondantes peuvent évoluer au fil du temps.
Énoncé initial du problème
Imaginez que nous ayons une boutique pour chats à notre disposition (car les chats sont l'animal le plus populaire de l'écosystème Scala). En raison des particularités de l'obtention d'informations sur les chats disponibles à la vente, dans certains cas, les informations ne proviennent pas de la base de données et doivent être triées manuellement avant d'envoyer une réponse HTTP au client ou de la traiter de toute autre manière. L'objet principal de la matière est, bien entendu, Cat
qui au départ ne comporte que trois champs de types primitifs. L'objectif est de développer une API pour trier les collections de chats répondant aux exigences suivantes:
- L'ordre de tri peut être déterminé pour chacun des champs.
- L'ordre de tri ne peut être défini pour aucun des champs
- Le tri doit être stable pour les champs avec un ordre de tri non défini
- (..
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 , .
. , , , . ( ) , , . , , , , , .
.
Bien que l'implémentation finale puisse sembler similaire dans d'autres langages de programmation, il me semble que Scala offre un bon compromis entre la sécurité des types et la compréhensibilité et la lisibilité du code. J'espère que cette publication a aidé à mieux comprendre comment trier les collections dans Scala, ou a suggéré des idées qui peuvent être appliquées dans du code qui n'est pas directement lié à Ordering
.
Tous les exemples de code sont disponibles dans ce référentiel github.