Cara idiomatik yang mudah untuk mendefinisikan Pengurutan untuk kelas kasus sederhana

110

Saya memiliki daftar contoh kelas kasus skala sederhana dan saya ingin mencetaknya dalam urutan leksikografis yang dapat diprediksi menggunakan list.sorted, tetapi menerima "Tidak ada Pengurutan implisit yang ditentukan untuk ...".

Apakah ada implisit yang menyediakan pengurutan leksikografis untuk kelas kasus?

Apakah ada cara idiomatik sederhana untuk menggabungkan pengurutan leksikografis ke dalam kelas kasus?

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)
            ^

Saya tidak senang dengan 'retasan':

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)
ya_pulser
sumber
8
Saya baru saja menulis posting blog dengan beberapa solusi umum di sini .
Travis Brown

Jawaban:

152

Metode favorit pribadi saya adalah menggunakan pemesanan implisit yang disediakan untuk Tuple, karena jelas, ringkas, dan benar:

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)
}

Ini bekerja karena pendamping untukOrdered mendefinisikan konversi implisit dari Ordering[T]ke Ordered[T]yang dalam lingkup untuk setiap kelas yang mengimplementasikan Ordered. Adanya implisit Orderings untuk Tuples memungkinkan konversi dari TupleN[...]menjadi Ordered[TupleN[...]]asalkan Ordering[TN]ada implisit untuk semua elemen T1, ..., TNtupel, yang seharusnya selalu menjadi kasus karena tidak masuk akal untuk mengurutkan pada tipe data dengan no Ordering.

Pengurutan implisit untuk Tuple adalah tujuan Anda untuk skenario pengurutan apa pun yang melibatkan kunci sortir komposit:

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

Karena jawaban ini telah terbukti populer, saya ingin memperluasnya, dengan memperhatikan bahwa solusi yang menyerupai berikut ini dalam beberapa keadaan dapat dianggap kelas perusahaan ™:

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)
}

Diberikan es: SeqLike[Employee], es.sorted()akan mengurutkan berdasarkan nama, dan es.sorted(Employee.orderingById)akan mengurutkan berdasarkan id. Ini memiliki beberapa manfaat:

  • Urutan ditentukan di satu lokasi sebagai artefak kode yang terlihat. Ini berguna jika Anda memiliki pengurutan yang rumit di banyak bidang.
  • Sebagian besar fungsionalitas pengurutan yang diimplementasikan di pustaka scala beroperasi menggunakan instance Ordering, jadi menyediakan pengurutan secara langsung menghilangkan konversi implisit dalam banyak kasus.
J Cracknell
sumber
Teladan Anda luar biasa! Single-liner dan saya memiliki pemesanan default. Terima kasih banyak.
ya_pulser
7
Kasus kelas A yang disarankan dalam jawaban tampaknya tidak dikompilasi di bawah skala 2.10. Apakah saya melewatkan sesuatu?
Doron Yaacoby
3
@DoronYaacoby: Saya juga mendapatkan kesalahan value compare is not a member of (String, Int).
bluenote10
1
@JCracknell Kesalahan masih ada bahkan setelah impor (Scala 2.10.4). Terjadi kesalahan selama kompilasi, tetapi tidak ditandai di IDE. (menariknya, ini berfungsi dengan baik di REPL). Bagi mereka yang memiliki masalah ini, solusi pada jawaban SO ini berfungsi (meski tidak seanggun di atas). Jika ini bug, apakah ada yang melaporkannya?
Jus12
2
MEMPERBAIKI: Cakupan Pengurutan tidak ditarik, Anda dapat secara implisit menariknya tetapi cukup mudah untuk hanya Menggunakan Pengurutan secara langsung: def bandingkan (bahwa: A) = Ordering.Tuple2 [String, String] .compare (tuple (ini ), tuple (itu))
brendon
46
object A {
  implicit val ord = Ordering.by(unapply)
}

Ini memiliki keuntungan bahwa itu diperbarui secara otomatis setiap kali A berubah. Namun, kolom A harus ditempatkan sesuai urutan penggunaannya.

IttayD
sumber
Tampak, keren, tapi saya tidak tahu cara menggunakan ini, saya mendapatkan:<console>:12: error: not found: value unapply
zbstof
29

Untuk meringkas, ada tiga cara untuk melakukan ini:

  1. Untuk penyortiran satu kali, gunakan metode .sortBy, seperti yang ditunjukkan @Shadowlands
  2. Untuk menggunakan kembali pengurutan, perluas kelas kasus dengan sifat Berurutan, seperti yang dikatakan @Keith.
  3. Tentukan pemesanan kustom. Manfaat dari solusi ini adalah Anda dapat menggunakan kembali urutan dan memiliki banyak cara untuk mengurutkan instance dari kelas yang sama:

    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))

Menjawab pertanyaan Anda Apakah ada fungsi standar yang dimasukkan ke dalam Scala yang dapat melakukan sihir seperti List ((2,1), (1,2)).

Ada sekumpulan urutan yang sudah ditentukan sebelumnya , misalnya untuk String, tupel hingga 9 arity dan seterusnya.

Tidak ada hal seperti itu untuk kelas kasus, karena itu bukan hal yang mudah untuk diluncurkan, mengingat nama bidang tidak dikenal secara a-priori (setidaknya tanpa sihir makro) dan Anda tidak dapat mengakses bidang kelas kasus dengan cara lain selain dengan nama / menggunakan iterator produk.

om-nom-nom
sumber
Terima kasih banyak atas contohnya. Saya akan mencoba untuk memahami urutan implisit.
ya_pulser
8

The unapplymetode dari objek pendamping menyediakan konversi dari kasus kelas Anda ke Option[Tuple], di mana Tupleadalah tuple sesuai dengan daftar argumen pertama dari kelas kasus. Dengan kata lain:

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

def sortPeople(people : List[Person]) = 
    people.sortBy(Person.unapply)
Joakim Ahnfelt-Rønne
sumber
6

Metode sortBy akan menjadi salah satu cara umum untuk melakukan ini, misalnya (urutkan di taglapangan):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)
Shadowlands
sumber
Apa yang harus dilakukan jika ada 3+ bidang dalam kelas kasus? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser
Jika menggunakan sortBy, maka ya, baik itu, atau tambahkan / gunakan fungsi yang sesuai untuk / pada kelas (misalnya _.toString, atau metode kustom atau fungsi eksternal yang signifikan secara leksografis Anda sendiri).
Shadowlands
Apakah ada fungsi standar yang termasuk dalam Scala yang dapat melakukan sihir seperti List((2,1),(1,2)).sortedobjek kelas kasus? Saya tidak melihat perbedaan besar antara tupel bernama (case class == bernama tuple) dan tupel sederhana.
ya_pulser
Yang terdekat yang bisa saya dapatkan sepanjang garis itu adalah dengan menggunakan metode unapply objek pendamping untuk mendapatkan Option[TupleN], lalu panggil getitu l.sortBy(A.unapply(_).get)foreach(println):, yang menggunakan urutan yang disediakan pada tupel yang sesuai, tetapi ini hanyalah contoh eksplisit dari gagasan umum yang saya berikan di atas .
Shadowlands
5

Karena Anda menggunakan kelas kasus, Anda dapat memperluas dengan Ordered seperti ini:

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
Keith Pinson
sumber