Olá Habr! Trago ao seu tribunal uma tradução russa do meu artigo no Medium: Sorting in Scala - um exemplo de loja de gatos . O artigo é destinado a leitores que conhecem a sintaxe da linguagem Scala e conhecem as ferramentas básicas da biblioteca padrão.
Apesar do Java e do Scala usarem a JVM como plataforma de tempo de execução, o Scala ganhou fama como uma linguagem muito mais expressiva, graças a uma adaptação significativa dos conceitos de programação funcional e uma rica biblioteca padrão. Neste artigo, considerarei um exemplo dessa expressividade, tentando imaginar como uma pequena parte da base de código e os requisitos correspondentes podem evoluir ao longo do tempo.
Declaração inicial do problema
Imagine que temos uma loja de gatos à nossa disposição (porque os gatos são o animal mais popular no ecossistema Scala). Devido às peculiaridades de obter informações sobre gatos disponíveis para venda, em alguns casos, as informações não provêm do banco de dados e devem ser classificadas manualmente antes de enviar uma resposta HTTP ao cliente ou processá-la de qualquer outra maneira. O principal objeto da área de assunto é, é claro, Catque no início possui apenas três campos de tipos primitivos. O objetivo é desenvolver uma API para classificar coleções de gatos que atenda aos seguintes requisitos:
- A ordem de classificação pode ser determinada para cada um dos campos.
- A ordem de classificação não pode ser definida para nenhum dos campos
- A classificação deve ser estável para campos com uma ordem de classificação indefinida
- (..
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 , .
. , , , . ( ) , , . , , , , , .
.
Embora a implementação final possa parecer semelhante em outras linguagens de programação, parece-me que o Scala fornece um bom compromisso entre a segurança de tipo e a compreensibilidade e legibilidade do código. Espero que esta publicação tenha ajudado a entender melhor como classificar coleções no Scala, ou sugeriu idéias que possam ser aplicadas em código que não esteja diretamente relacionado Ordering.
Todo o código de amostra está disponível neste repositório do github.