Manera idiomática fácil de definir Ordering para una clase de caso simple

110

Tengo una lista de instancias simples de clases de casos de Scala y quiero imprimirlas en un orden lexicográfico predecible usando list.sorted, pero recibo "Sin orden implícito definido para ...".

¿Existe un implícito que proporcione un orden lexicográfico para las clases de casos?

¿Existe una forma idiomática simple de mezclar el orden lexicográfico en la clase de caso?

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

No estoy contento con un 'truco':

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)
ya_pulser
fuente
8
Acabo de escribir una publicación de blog con un par de soluciones genéricas aquí .
Travis Brown

Respuestas:

152

Mi método favorito personal es hacer uso del orden implícito proporcionado para Tuples, ya que es claro, conciso y correcto:

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

Esto funciona porque el compañero deOrdered define una conversión implícita de Ordering[T]a Ordered[T]que está dentro del alcance de la implementación de cualquier clase Ordered. La existencia de Orderings implícitos para Tuples permite una conversión deTupleN[...] a Ordered[TupleN[...]]siempre que Ordering[TN]exista un implícito para todos los elementos T1, ..., TNde la tupla, lo que siempre debería ser el caso porque no tiene sentido ordenar un tipo de datos con no Ordering.

El orden implícito para Tuples es su opción para cualquier escenario de clasificación que involucre una clave de clasificación compuesta:

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

Como esta respuesta ha demostrado ser popular, me gustaría ampliarla, señalando que una solución similar a la siguiente podría, en algunas circunstancias, considerarse de nivel empresarial ™:

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

Dado es: SeqLike[Employee],es.sorted() ordenará por nombre y es.sorted(Employee.orderingById)ordenará por id. Esto tiene algunos beneficios:

  • Las clases se definen en una única ubicación como artefactos de código visibles. Esto es útil si tiene ordenaciones complejas en muchos campos.
  • La mayoría de las funciones de ordenación implementadas en la biblioteca scala operan utilizando instancias de Ordering, por lo que proporcionar una ordenación directamente elimina una conversión implícita en la mayoría de los casos.
J Cracknell
fuente
¡Tu ejemplo es maravilloso! Revestimiento único y tengo pedidos predeterminados. Muchas gracias.
ya_pulser
7
La clase de caso A sugerida en la respuesta no parece compilarse en scala 2.10. ¿Me estoy perdiendo de algo?
Doron Yaacoby
3
@DoronYaacoby: También recibo un error value compare is not a member of (String, Int).
bluenote10
1
@JCracknell El error sigue ahí incluso después de la importación (Scala 2.10.4). Se produce un error durante la compilación, pero no se marca en IDE. (Curiosamente, funciona correctamente en REPL). Para aquellos que tienen este problema, la solución en esta respuesta SO funciona (aunque no tan elegante como la anterior). Si es un error, ¿alguien lo ha informado?
Jus12
2
REVISIÓN: El alcance de Ordering no se está introduciendo, puede hacerlo implícitamente, pero es bastante fácil usar Ordering directamente: def compare (that: A) = Ordering.Tuple2 [String, String] .compare (tuple (this ), tupla (que))
brendon
46
object A {
  implicit val ord = Ordering.by(unapply)
}

Esto tiene la ventaja de que se actualiza automáticamente cada vez que A cambia. Sin embargo, los campos de A deben colocarse en el orden en que los utilizará el pedido.

IttayD
fuente
Parece genial, pero no puedo entender cómo usar esto, obtengo:<console>:12: error: not found: value unapply
zbstof
29

Para resumir, hay tres formas de hacer esto:

  1. Para una clasificación única, use el método .sortBy, como lo ha demostrado @Shadowlands
  2. Para reutilizar la clasificación, extienda la clase de caso con el rasgo Ordered, como dijo @Keith.
  3. Defina un pedido personalizado. El beneficio de esta solución es que puede reutilizar los pedidos y tener varias formas de ordenar instancias de la misma clase:

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

Respondiendo tu pregunta ¿Existe alguna función estándar incluida en Scala que pueda hacer magia como List ((2,1), (1,2)). Sorted

Hay un conjunto de ordenamientos predefinidos , por ejemplo, para String, tuplas hasta 9 arity y así sucesivamente.

No existe tal cosa para las clases de casos, ya que no es fácil de rodar, dado que los nombres de campo no se conocen a priori (al menos sin la magia de macros) y no puede acceder a los campos de clases de casos de otra manera que no sea por nombre / usando iterador de producto.

Om nom nom
fuente
Muchas gracias por los ejemplos. Intentaré comprender el orden implícito.
ya_pulser
8

El unapplymétodo del objeto complementario proporciona una conversión de su clase de caso a an Option[Tuple], donde Tuplees la tupla correspondiente a la primera lista de argumentos de la clase de caso. En otras palabras:

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

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

El método sortBy sería una forma típica de hacer esto, por ejemplo (ordenar por tagcampo):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)
Tierras Sombrías
fuente
¿Qué hacer en caso de 3+ campos en la clase de caso? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser
Si usa sortBy, entonces sí, eso, o agregar / usar una función adecuada a / en la clase (por ejemplo _.toString, o su propio método personalizado o función externa significativa lexográficamente).
Shadowlands
¿Hay alguna función estándar incluida en Scala que pueda hacer magia List((2,1),(1,2)).sortedcon los objetos de la clase case? No veo una gran diferencia entre las tuplas con nombre (clase de caso == tupla con nombre) y las tuplas simples.
ya_pulser
Lo más cerca que puedo llegar a lo largo de esas líneas es usar el método unapply del objeto complementario para obtener un Option[TupleN], luego llamar geta eso:, l.sortBy(A.unapply(_).get)foreach(println)que usa el orden proporcionado en la tupla correspondiente, pero esto es simplemente un ejemplo explícito de la idea general que doy arriba .
Shadowlands
5

Como usó una clase de caso, puede extender con Ordered como tal:

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
fuente