Tri à Scala - un exemple sur les chats

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, Catqui 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. , ()
  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 , .


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


.



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.


All Articles