Hola Habr! Traigo a su corte una traducción al ruso de mi artículo sobre Medium: Sorting in Scala, un ejemplo de tienda de gatos . El artículo está dirigido a lectores que conocen la sintaxis del lenguaje Scala y conocen las herramientas básicas de la biblioteca estándar.
A pesar del hecho de que tanto Java como Scala utilizan JVM como plataforma de tiempo de ejecución, Scala ha ganado fama como un lenguaje mucho más expresivo, gracias a una adaptación significativa de conceptos de programación funcional y una biblioteca estándar rica. En este artículo consideraré un ejemplo de esta expresividad, tratando de imaginar cómo una pequeña parte de la base del código y los requisitos correspondientes pueden evolucionar con el tiempo.
Declaración inicial del problema.
Imagine que tenemos una tienda de gatos a nuestra disposición (porque los gatos son el animal más popular en el ecosistema Scala). Debido a las peculiaridades de obtener información sobre los gatos disponibles para la venta, en algunos casos la información no proviene de la base de datos y debe clasificarse manualmente antes de enviar una respuesta HTTP al cliente o procesarla de cualquier otra manera. El objeto principal del área temática es, por supuesto, Cat
que al principio tiene solo tres campos de tipos primitivos. El objetivo es desarrollar una API para ordenar colecciones de gatos que cumpla con los siguientes requisitos:
- El orden de clasificación se puede determinar para cada uno de los campos.
- El orden de clasificación puede no estar definido para ninguno de los campos
- La ordenación debe ser estable para los campos con un orden de clasificación indefinido
- (..
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 , .
. , , , . ( ) , , . , , , , , .
.
Aunque la implementación final puede parecer similar en otros lenguajes de programación, me parece que Scala ofrece un buen compromiso entre la seguridad de tipos y la comprensión y legibilidad del código. Espero que esta publicación haya ayudado a comprender mejor cómo ordenar colecciones en Scala, o ideas sugeridas que se pueden aplicar en código que no está directamente relacionado Ordering
.
Todo el código de muestra está disponible en este repositorio de github.