Facile modo idiomatico per definire l'ordinamento per una semplice classe di casi


110

Ho un elenco di istanze di classe scala case semplici e desidero stamparle in un ordine lessicografico prevedibile utilizzando list.sorted, ma ricevo "Nessun ordine implicito definito per ...".

Esiste un implicito che fornisce un ordinamento lessicografico per le classi di casi?

C'è un modo semplice e idiomatico per mescolare l'ordinamento lessicografico nella classe dei casi?

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

Non sono soddisfatto di un "hack":

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

8
Ho appena scritto un post sul blog con un paio di soluzioni generiche qui .
Travis Brown

Risposte:


152

Il mio metodo preferito personale è quello di utilizzare l'ordinamento implicito fornito per le tuple, poiché è chiaro, conciso e corretto:

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

Questo funziona perché il compagno perOrdered definisce una conversione implicita da Ordering[T]a Ordered[T]che è nell'ambito di qualsiasi implementazione di classe Ordered. L'esistenza di implicit Orderings per Tuples consente una conversione da TupleN[...]a Ordered[TupleN[...]]purché Ordering[TN]esista un implicito per tutti gli elementi T1, ..., TNdella tupla, il che dovrebbe sempre essere il caso perché non ha senso ordinare su un tipo di dati con no Ordering.

L'ordinamento implicito per le tuple è il tuo punto di riferimento per qualsiasi scenario di ordinamento che coinvolga una chiave di ordinamento composta:

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

Poiché questa risposta si è dimostrata popolare, vorrei approfondirla, osservando che una soluzione simile alla seguente potrebbe in alcune circostanze essere considerata di livello aziendale ™:

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

Dato es: SeqLike[Employee], es.sorted()ordinerà per nome e es.sorted(Employee.orderingById)ordinerà per ID. Questo ha alcuni vantaggi:

  • Gli ordinamenti vengono definiti in un'unica posizione come artefatti di codice visibili. Ciò è utile se disponi di ordinamenti complessi su molti campi.
  • La maggior parte delle funzionalità di ordinamento implementate nella libreria scala opera utilizzando istanze di Ordering, quindi fornire un ordinamento direttamente elimina una conversione implicita nella maggior parte dei casi.

Il tuo esempio è meraviglioso! Single-liner e ho un ordine predefinito. Grazie mille.
ya_pulser

7
La classe di casi A suggerita nella risposta non sembra compilarsi in scala 2.10. Mi sto perdendo qualcosa?
Doron Yaacoby

3
@ DoronYaacoby: ricevo anche un errore value compare is not a member of (String, Int).
bluenote10

1
@JCracknell L'errore è ancora presente anche dopo l'importazione (Scala 2.10.4). Si verifica un errore durante la compilazione, ma non viene contrassegnato nell'IDE. (È interessante notare che funziona correttamente in REPL). Per coloro che hanno questo problema, la soluzione a questa risposta SO funziona (anche se non elegante come sopra). Se si tratta di un bug, qualcuno l'ha segnalato?
Jus12

2
FIX: L'ambito di Ordering non viene inserito, puoi implicitamente inserirlo ma è abbastanza facile usare direttamente Ordering: def compare (that: A) = Ordering.Tuple2 [String, String] .compare (tuple (this ), tuple (that))
brendon

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

Ciò ha il vantaggio che viene aggiornato automaticamente ogni volta che A cambia. Tuttavia, i campi di A devono essere inseriti nell'ordine in cui verranno utilizzati dall'ordinamento.


Sembra fantastico, ma non riesco a capire come usarlo, ottengo:<console>:12: error: not found: value unapply
zbstof

29

Per riassumere, ci sono tre modi per farlo:

  1. Per l'ordinamento una tantum usa il metodo .sortBy, come ha mostrato @Shadowlands
  2. Per riutilizzare l'ordinamento estendi la classe case con il tratto Ordered, come ha detto @Keith.
  3. Definisci un ordine personalizzato. Il vantaggio di questa soluzione è che puoi riutilizzare gli ordini e avere diversi modi per ordinare le istanze della stessa classe:

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

Rispondere alla tua domanda C'è qualche funzione standard inclusa in Scala che può fare magie come List ((2,1), (1,2)).

C'è una serie di ordinamenti predefiniti , ad esempio per String, tuple fino a 9 arity e così via.

Non esiste nulla di simile per le classi case, dal momento che non è facile da eseguire, dato che i nomi dei campi non sono noti a priori (almeno senza la magia delle macro) e non è possibile accedere ai campi delle classi case in un modo diverso da nome / utilizzando l'iteratore del prodotto.


Grazie mille per gli esempi. Cercherò di capire l'ordinamento implicito.
ya_pulser

8

Il unapplymetodo dell'oggetto associato fornisce una conversione dalla classe case in an Option[Tuple], dove Tupleè la tupla corrispondente al primo elenco di argomenti della classe case. In altre parole:

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

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

6

Il metodo sortBy sarebbe un modo tipico per farlo, ad esempio (ordina sul tagcampo):

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

Cosa fare in caso di 3+ ​​campi nella classe case? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser

Se si utilizza sortBy, allora sì, o aggiungere / utilizzare una funzione adatta alla / sulla classe (ad esempio _.toString, o il proprio metodo personalizzato significativo dal punto di vista lessicale o una funzione esterna).
Shadowlands

C'è qualche funzione standard inclusa in Scala che può fare magie come List((2,1),(1,2)).sortedgli oggetti della classe case? Non vedo grandi differenze tra le tuple con nome (classe case == tuple con nome) e le tuple semplici.
ya_pulser

Il modo più vicino che posso ottenere in questo senso è usare il metodo unapply dell'oggetto associato per ottenere un Option[TupleN], quindi chiamare getquello :, l.sortBy(A.unapply(_).get)foreach(println)che utilizza l'ordinamento fornito sulla tupla corrispondente, ma questo è semplicemente un esempio esplicito dell'idea generale che ho dato sopra .
Shadowlands

5

Dato che hai usato una classe case puoi estenderla con Ordered in questo modo:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.