Pull to refresh

Comments 9

Если кодировать порядок в АДТ не является самоцелью все эти требования легко удовлетворить двумя функциями:


  def cats[T: Ordering](f: Cat => T): Ordering[Cat] = Ordering.by(f)

  def nullsLast[T](implicit t: Ordering[T]): Ordering[Option[T]] = Ordering.Option(t.reverse).reverse

И то, первая нужна только чтобы не указывать типы вручную.


В результате нужный порядок описывается так:


cats(_.breed) thenComparing 
cats(_.age).reversed thenComparing 
cats(_.owner)(nullsLast)

Запись действительно очень лаконичная, спасибо за идею. Только, кажется, в конце должно быть nullsLast(cats(_.owner))


Тем не менее, при подобном подходе вижу следующие ограничения:


  • Чтобы определить порядок сортировки по тому или иному полю, придётся перебирать каждое поле в отдельности.
  • Изменение приоритета полей тоже будет осуществляться с помощью перебора.
  • Добавление/удаление поля потребует изменения кода, связанного с сортировкой.

Иными словами, попробуйте представить, имея условный запрос от пользователя на сортировку данных, как предполагается этот запрос реализовывать в вашем подходе?

Нет, должно быть cats(_.owner)(nullsLast) так как nullsLast заменяет собой неявный порядок по-умолчанию.


Что вы имеете в виду говоря про "перебор"?

Ах, верно, я в самом деле не доглядел. Да, в таком случае она дополнится функцией nullsFirst и итог будет симметричен функционалу ADT, вы абсолютно правы.


По поводу же перебора, я имею в виду, что, поскольку вы получаете поля явно (без использования аналога CatField), то конечная реализация, соответветствующая требованиям третьей итерации, будет выглядеть приблизительно так (scastie):


import scala.math.Ordering

object CatOrdering {

  case class Cat(breed: String,
                 /*...*/
                 owner: Option[String],
                 /*...*/)

  type FieldName = String

  // 1 bool: order -> asc - true, desc - false
  // 2 bool: empty values first -> asc - true, desc - false
  def byFields(fields: Seq[(FieldName, Option[(Boolean, Boolean)])]): Ordering[Cat] = {
    val breedOrder = get("breed", fields)
    val breedOrdering = "breed" -> breedOrder.map { case (order, _) => field(_.breed, order) }

    // ...

    val ownerOrder = get("owner", fields)
    val ownerOrdering = "owner" -> ownerOrder.map { case (order, emptyRule) => field(_.owner, order, emptyRule) } 

    // ...

    val orderings: Map[FieldName, Ordering[Cat]] = Map(
      breedOrdering, 
      /*...*/
      ownerOrdering, 
      /*...*/
    ).collect { case (key, Some(ordering)) => key -> ordering }

    if (fields.isEmpty)
      Ordering.by(_ => 0)
    else {
      val h = orderings(fields.head._1)
      fields.tail.foldLeft(h) { case (acc, (key, _)) => acc.orElse(orderings(key)) }
    }
  }

  private def get(fieldName: FieldName, seq: Seq[(FieldName, Option[(Boolean, Boolean)])]): Option[(Boolean, Boolean)] = ???

  private def field[T: Ordering](f: Cat => T, asc: Boolean): Ordering[Cat] = ???

  private def field[T: Ordering](f: Cat => Option[T], asc: Boolean, emptyValuesFirst: Boolean): Ordering[Cat] = ???
}

Если же вы имеете в виду только SortOrder, то без его аналога реализация так или иначе будет вынуждена использовать (Boolean, Boolean). Насколько это хорошо — вопрос дискуссионный, но я придерживаюсь мнения, что в данном случае ADT сделает код более читаемым и, следовательно, поддерживаемым.


К слову, касательно именно Boolean существует мнение, что их нужно выражать через функции, а не ADT — Destroy All Ifs — A Perspective from Functional Programming.

Если необходимо формировать порядок полей в рантайме, то я бы предпочел вместо объявления объектов на каждое поле объявить мапу Map[String, SortField] — все равно поля вероятно прийдут в виде строк.


Делать отдельные кейсы для Asc/Desc/Keep мне кажется легким оверинжинирингом когда можно обойтись Order(asc: Boolean, nullsFirst: Boolean) или Option[Order] если очень нужен Keep.


sealed trait SortField[E]
object SortField {
  case class Optional[E, T](field: E => Option[T], order: Ordering[T])
    extends SortField[E]

  case class Mandatory[E, T](field: E => T, order: Ordering[T])
    extends SortField[E]

  def apply[E, T: Ordering](f: E => T): SortField[E] =
    Mandatory(f, Ordering[T])
  def optional[E, T: Ordering](f: E => Option[T]): SortField[E] =
    Optional(f, Ordering[T])
}

case class SortOrder(asc: Boolean = true, nullsFirst: Boolean = true)

def orderBy[E](fields: (SortField[E], SortOrder)*): Ordering[E] = {
  def initial = Ordering.by[E, Int](_ => 0)
  def direction[T](ordering: Ordering[T], asc: Boolean) =
    if (asc) ordering else ordering.reverse

  val comparator = fields
    .map {
      case (SortField.Mandatory(field, ordering), SortOrder(asc, _)) =>
        direction(Ordering.by(field)(ordering), asc)

      case (SortField.Optional(field, ordering), SortOrder(asc, true)) =>
        Ordering.by(field)(Ordering.Option(direction(ordering, asc)))

      case (SortField.Optional(field, ordering), SortOrder(asc, false)) =>
        val order = direction(Ordering.Option(direction(ordering, !asc)), asc = false)
        Ordering.by(field)(order)
    }
    .foldLeft(initial: Comparator[E])(_ thenComparing _)
  Ordering.comparatorToOrdering(comparator)
}

Предложенное вами решение мне очень нравится из-за возможности применения к любому типу, а не только Cat. Большое спасибо вам за идею! В конце, вроде бы, можно воспользоваться foldLeft(initial)(_ orElse _).


Касательно Keep — он был нужен только в первых двух случаях, в третьей итерации он отсутствует.


По поводу полей — это уже больше вопрос десериализации, я согласен, просто объекты позволят вывести кодек автоматически.

Увы, Ordering.orElse появился только в 13 скале, мой код на 12

Sign up to leave a comment.

Articles