Легкий ідіоматичний спосіб визначення замовлення для простого класу випадків


110

У мене є список простих екземплярів класу шкали шкали, і я хочу надрукувати їх у передбачуваному, лексикографічному порядку list.sorted, але отримую "Немає неявного впорядкування, визначеного для ...".

Чи існує неявна неявка, яка забезпечує лексикографічне упорядкування для класів випадків?

Чи є простий ідіоматичний спосіб поєднання лексикографічного впорядкування в регістр класів?

scala> case class A(tag:String, load:Int)
scala> val l = List(A("words",50),A("article",2),A("lines",7))

scala> l.sorted.foreach(println)
<console>:11: error: No implicit Ordering defined for A.
          l.sorted.foreach(println)
            ^

Я не задоволений "хаком":

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)

8
Я щойно написав повідомлення в блозі з парою загальних рішень тут .
Тревіс Браун

Відповіді:


152

Мій особистий улюблений метод - використовувати надане неявне замовлення для Tuples, як це зрозуміло, стисло та правильно:

case class A(tag: String, load: Int) extends Ordered[A] {
  // Required as of Scala 2.11 for reasons unknown - the companion to Ordered
  // should already be in implicit scope
  import scala.math.Ordered.orderingToOrdered

  def compare(that: A): Int = (this.tag, this.load) compare (that.tag, that.load)
}

Це працює тому , що супутникOrdered визначає неявне перетворення з Ordering[T]в Ordered[T]якому знаходиться в області видимості для будь-якого класу , що реалізує Ordered. Існування неявних Orderings для Tuples дає змогу перетворитись TupleN[...]на Ordered[TupleN[...]]умовне, що Ordering[TN]існує неявне для всіх елементівT1, ..., TN кортежу, що завжди має бути так, оскільки немає сенсу сортувати за типом даних без Ordering.

Неявне впорядкування для Tuples - це перехід до будь-якого сценарію сортування, що включає складений ключ сортування:

as.sortBy(a => (a.tag, a.load))

Оскільки ця відповідь виявилася популярною, я хотів би розширити її, зазначивши, що рішення, що нагадує таке, за певних обставин може вважатися корпоративним ™:

case class Employee(id: Int, firstName: String, lastName: String)

object Employee {
  // Note that because `Ordering[A]` is not contravariant, the declaration
  // must be type-parametrized in the event that you want the implicit
  // ordering to apply to subclasses of `Employee`.
  implicit def orderingByName[A <: Employee]: Ordering[A] =
    Ordering.by(e => (e.lastName, e.firstName))

  val orderingById: Ordering[Employee] = Ordering.by(e => e.id)
}

Дано es: SeqLike[Employee], es.sorted()буде сортувати за назвою таes.sorted(Employee.orderingById) сортуватимуть за ідентифікатором. Це має кілька переваг:

  • Сорти визначаються в одному місці як видимі артефакти коду. Це корисно, якщо у вас є складні сорти в багатьох полях.
  • Більшість функцій сортування, реалізованих у бібліотеці scala, функціонують із застосуванням екземплярів Ordering, тому надання замовлення безпосередньо виключає неявне перетворення у більшості випадків.

Ваш приклад чудовий! Однолінійний і я замовляю за замовчуванням. Велике спасибі.
ya_pulser

7
Клас випадку A, запропонований у відповіді, не здається складеним за шкалою 2.10. Я щось пропускаю?
Дорон Якобі

3
@DoronYaacoby: Я також отримую помилку value compare is not a member of (String, Int).
bluenote10

1
@JCracknell Помилка все ще є навіть після імпорту (Scala 2.10.4). Помилка виникає під час компіляції, але не позначається в IDE. (що цікаво, вона справно працює в REPL). Для тих, хто має цю проблему, рішення на цю відповідь ТА спрацьовує (хоча не настільки елегантно, як вище). Якщо це помилка, хтось повідомляв про це?
Jus12

2
Виправлення: сфера впорядкування не втягується, ви можете неявно втягнути її, але досить просто просто скористатись замовленням: def порівняти (що: A) = ordering.Tuple2 [String, String] .compare (tuple (це ), кортеж (що))
бренд

46
object A {
  implicit val ord = Ordering.by(unapply)
}

Це має ту перевагу, що воно оновлюється автоматично, коли A змінюється. Але, поля A потрібно розмістити в тому порядку, в якому замовлення буде використовувати їх.


Здається, круто, але я не можу зрозуміти, як це використовувати, я отримую:<console>:12: error: not found: value unapply
zbstof

29

Підводячи підсумок, є три способи зробити це:

  1. Для разового сортування використовують метод .sortBy, як показав @Shadowlands
  2. Для повторного використання сортування розширюйте клас регістру з упорядкованою ознакою, як сказав @Keith.
  3. Визначте спеціальне замовлення. Перевага цього рішення полягає в тому, що ви можете повторно використовувати замовлення та мати кілька способів сортування примірників одного класу:

    case class A(tag:String, load:Int)
    
    object A {
      val lexicographicalOrdering = Ordering.by { foo: A => 
        foo.tag 
      }
    
      val loadOrdering = Ordering.by { foo: A => 
        foo.load 
      }
    }
    
    implicit val ord = A.lexicographicalOrdering 
    val l = List(A("words",1), A("article",2), A("lines",3)).sorted
    // List(A(article,2), A(lines,3), A(words,1))
    
    // now in some other scope
    implicit val ord = A.loadOrdering
    val l = List(A("words",1), A("article",2), A("lines",3)).sorted
    // List(A(words,1), A(article,2), A(lines,3))

Відповідь на ваше запитання Чи є в Scala якась стандартна функція, яка може виконувати магію, як List ((2,1), (1,2)).

Існує набір заздалегідь визначених замовлень , наприклад, для String, кортежів до 9 аритів тощо.

Для класів випадків такого не існує, оскільки скасувати це непросто, враховуючи, що назви полів не відомі априорі (принаймні, без магії макросів), і ви не можете отримати доступ до полів класу case іншим способом, ніж назва / за допомогою ітератора продукту.


Дуже дякую за приклади. Я спробую зрозуміти неявне впорядкування.
ya_pulser

8

unapplyМетод об'єкта - компаньйон забезпечує перетворення з вашого випадку класу до Option[Tuple], де Tupleє кортеж , відповідний першого список аргументів класу справи. Іншими словами:

case class Person(name : String, age : Int, email : String)

def sortPeople(people : List[Person]) = 
    people.sortBy(Person.unapply)

6

Метод sortBy був би одним типовим способом цього, наприклад (сортувати по tagполю):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)

Що робити у випадку 3+ полів у класі регістру? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser

Якщо ви користуєтесь sortBy, то так, або це, або додайте / використовуйте відповідну функцію класу / на клас (наприклад _.toString, або власний лексографічно значимий спеціальний метод або зовнішню функцію).
Shadowlands

Чи є в Scala якась стандартна функція, яка може виконувати магію, як List((2,1),(1,2)).sortedоб'єкти класу case? Я не бачу великої різниці між названими кортежами (клас класів == названий кортеж) та простими кортежами.
ya_pulser

Найближче, що я можу пройти через ці рядки, - це використовувати метод непридатності супутнього об'єкта, щоб отримати Option[TupleN], а потім зателефонувати getна що:, l.sortBy(A.unapply(_).get)foreach(println)який використовує надане впорядкування у відповідному кортежі, але це просто явний приклад загальної ідеї, яку я даю вище .
Shadowlands

5

Оскільки ви використовували клас регістру, ви можете розширити такий порядок :

case class A(tag:String, load:Int) extends Ordered[A] { 
  def compare( a:A ) = tag.compareTo(a.tag) 
}

val ls = List( A("words",50), A("article",2), A("lines",7) )

ls.sorted
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.