Manière idiomatique simple de définir la commande pour une classe de cas simple

110

J'ai une liste d'instances de classe de cas scala simples et je veux les imprimer dans un ordre lexicographique prévisible en utilisant list.sorted, mais je reçois "Aucun ordre implicite défini pour ...".

Existe-t-il un implicite qui fournit un ordre lexicographique pour les classes de cas?

Existe-t-il un moyen idiomatique simple de mélanger l'ordre lexicographique dans la classe de cas?

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

Je ne suis pas content d'un 'hack':

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)
ya_pulser
la source
8
Je viens de rédiger un article de blog avec quelques solutions génériques ici .
Travis Brown

Réponses:

152

Ma méthode préférée personnelle est d'utiliser la commande implicite fournie pour les tuples, car elle est claire, concise et correcte:

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

Cela fonctionne car le compagnon deOrdered définit une conversion implicite de Ordering[T]vers Ordered[T]laquelle est dans la portée de toute implémentation de classe Ordered. L'existence de Orderings implicite pour Tuples permet une conversion de à TupleN[...]à Ordered[TupleN[...]]condition qu'un implicite Ordering[TN]existe pour tous les éléments T1, ..., TNdu tuple, ce qui devrait toujours être le cas car il n'a aucun sens de trier sur un type de données sans Ordering.

L'ordre implicite pour les tuples est votre choix pour tout scénario de tri impliquant une clé de tri composite:

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

Comme cette réponse s'est avérée populaire, j'aimerais m'étendre là-dessus, en notant qu'une solution semblable à la suivante pourrait dans certaines circonstances être considérée comme de qualité entreprise ™:

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

Étant donné es: SeqLike[Employee], es.sorted()triera par nom et es.sorted(Employee.orderingById)triera par identifiant. Cela présente quelques avantages:

  • Les tris sont définis dans un emplacement unique comme des artefacts de code visibles. Ceci est utile si vous avez des tris complexes sur de nombreux champs.
  • La plupart des fonctionnalités de tri implémentées dans la bibliothèque scala fonctionnent à l'aide d'instances de Ordering, donc fournir un ordre élimine directement une conversion implicite dans la plupart des cas.
J Cracknell
la source
Votre exemple est merveilleux! Single-liner et j'ai la commande par défaut. Merci beaucoup.
ya_pulser
7
La classe de cas A suggérée dans la réponse ne semble pas compiler sous scala 2.10. Est-ce que je manque quelque chose?
Doron Yaacoby
3
@DoronYaacoby: J'obtiens également une erreur value compare is not a member of (String, Int).
bluenote10
1
@JCracknell L'erreur est toujours là même après l'importation (Scala 2.10.4). Une erreur se produit lors de la compilation, mais n'est pas signalée dans l'EDI. (Fait intéressant, cela fonctionne correctement dans REPL). Pour ceux qui ont ce problème, la solution à cette réponse SO fonctionne (mais pas aussi élégante que celle ci-dessus). S'il s'agit d'un bug, quelqu'un l'a-t-il signalé?
Jus12
2
CORRECTIF: La portée de Ordering n'est pas tirée, vous pouvez la retirer implicitement, mais il est assez facile d'utiliser simplement Ordering directement: def compare (that: A) = Ordering.Tuple2 [String, String] .compare (tuple (this ), tuple (that))
brendon
46
object A {
  implicit val ord = Ordering.by(unapply)
}

Cela présente l'avantage d'être mis à jour automatiquement chaque fois que A change. Mais les champs de A doivent être placés dans l'ordre dans lequel la commande les utilisera.

IttayD
la source
Ça a l'air cool, mais je n'arrive pas à comprendre comment l'utiliser, je reçois:<console>:12: error: not found: value unapply
zbstof
29

Pour résumer, il existe trois façons de procéder:

  1. Pour un tri ponctuel, utilisez la méthode .sortBy, comme @Shadowlands l'a montré
  2. Pour la réutilisation du tri, étendez la classe de cas avec le trait Ordered, comme l'a dit @Keith.
  3. Définissez une commande personnalisée. L'avantage de cette solution est que vous pouvez réutiliser les classements et avoir plusieurs façons de trier les instances de la même 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))
    

Répondre à votre question Y a-t-il une fonction standard incluse dans la Scala qui peut faire de la magie comme List ((2,1), (1,2)). Sorted

Il existe un ensemble de classements prédéfinis , par exemple pour String, des tuples jusqu'à 9 arités et ainsi de suite.

Rien de tel n'existe pour les classes de cas, car ce n'est pas chose facile à faire, étant donné que les noms de champs ne sont pas connus a-priori (au moins sans la magie des macros) et que vous ne pouvez pas accéder aux champs de classe de cas d'une manière autre que par nom / utilisation de l'itérateur de produit.

Om Nom Nom
la source
Merci beaucoup pour les exemples. J'essaierai de comprendre l'ordre implicite.
ya_pulser
8

La unapplyméthode de l'objet compagnon fournit une conversion de votre classe de cas en an Option[Tuple], où le Tupleest le tuple correspondant à la première liste d'arguments de la classe de cas. En d'autres termes:

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

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

La méthode sortBy serait une façon typique de faire cela, par exemple (trier sur tagchamp):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)
Shadowlands
la source
Que faire en cas de 3+ champs dans la classe de cas? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser
Si vous utilisez sortBy, alors oui, soit cela, soit ajoutez / utilisez une fonction appropriée à / sur la classe (par exemple _.toString, ou votre propre méthode personnalisée ou fonction externe lexographiquement significative).
Shadowlands
Y a-t-il une fonction standard incluse dans la Scala qui peut faire de la magie comme List((2,1),(1,2)).sortedles objets de la classe case? Je ne vois pas de grande différence entre les tuples nommés (classe de cas == tuple nommé) et les tuples simples.
ya_pulser
Le plus proche que je puisse obtenir le long de ces lignes est d'utiliser la méthode unapply de l'objet compagnon pour obtenir un Option[TupleN], puis appelez- getle :, l.sortBy(A.unapply(_).get)foreach(println)qui utilise l'ordre fourni sur le tuple correspondant, mais il s'agit simplement d'un exemple explicite de l'idée générale que je donne ci-dessus .
Shadowlands
5

Puisque vous avez utilisé une classe de cas, vous pouvez l'étendre avec Ordered comme ceci :

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
la source