Comments 9
За статью, конечно, спасибо! Идея интересная.
"Пример на кошках" — думал, тут что-то про Cats.
Если кодировать порядок в АДТ не является самоцелью все эти требования легко удовлетворить двумя функциями:
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
— он был нужен только в первых двух случаях, в третьей итерации он отсутствует.
По поводу полей — это уже больше вопрос десериализации, я согласен, просто объекты позволят вывести кодек автоматически.
Сортировка в Scala — пример на кошках