Łatwy idiomatyczny sposób definiowania kolejności dla prostej klasy przypadku

110

Mam listę prostych instancji klas przypadków scala i chcę je wydrukować w przewidywalnej, leksykograficznej kolejności przy użyciu list.sorted, ale otrzymuję „Brak domyślnego porządku zdefiniowanego dla ...”.

Czy istnieje niejawne, które zapewnia porządkowanie leksykograficzne dla klas przypadków?

Czy istnieje prosty idiomatyczny sposób na dodanie porządku leksykograficznego do klasy przypadków?

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

Nie podoba mi się „hack”:

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)
ya_pulser
źródło
8
Właśnie spisane blogu z kilku rozwiązań generycznych tutaj .
Travis Brown

Odpowiedzi:

152

Moją ulubioną metodą jest skorzystanie z dostarczonej niejawnej kolejności dla krotek, ponieważ jest ona jasna, zwięzła i poprawna:

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

To działa, ponieważ towarzyszOrdered definiuje niejawna konwersja z Ordering[T]celu Ordered[T], który jest w zakresie do dowolnej klasy wykonawczego Ordered. Istnienie niejawnego Orderings dla Tuples umożliwia konwersję z TupleN[...]do Ordered[TupleN[...]]pod warunkiem, że niejawne Ordering[TN]istnieje dla wszystkich elementów T1, ..., TNkrotki, co zawsze powinno mieć miejsce, ponieważ nie ma sensu sortować według typu danych bez Ordering.

Niejawne porządkowanie dla krotek jest Twoim celem dla każdego scenariusza sortowania obejmującego złożony klucz sortowania:

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

Ponieważ ta odpowiedź stała się popularna, chciałbym ją rozwinąć, zauważając, że rozwiązanie podobne do poniższego może w pewnych okolicznościach zostać uznane za klasy korporacyjnej ™:

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

Podane es: SeqLike[Employee], es.sorted()posortuje według nazwy i es.sorted(Employee.orderingById)posortuje według identyfikatora. Ma to kilka zalet:

  • Sortowania są zdefiniowane w jednym miejscu jako widoczne artefakty kodu. Jest to przydatne, jeśli masz złożone sortowanie na wielu polach.
  • Większość funkcji sortowania zaimplementowanych w bibliotece scala działa przy użyciu wystąpień Ordering, więc podanie kolejności bezpośrednio eliminuje niejawną konwersję w większości przypadków.
J Cracknell
źródło
Twój przykład jest wspaniały! Single-liner i mam domyślne zamówienie. Dziękuję Ci bardzo.
ya_pulser
7
Klasa przypadku A sugerowana w odpowiedzi nie wydaje się kompilować w scali 2.10. Czy coś mi brakuje?
Doron Yaacoby
3
@DoronYaacoby: Również pojawia się błąd value compare is not a member of (String, Int).
bluenote10
1
@JCracknell Błąd nadal występuje nawet po imporcie (Scala 2.10.4). Błąd występuje podczas kompilacji, ale nie jest oznaczany w IDE. (co ciekawe, działa poprawnie w REPL). Dla tych, którzy mają ten problem, rozwiązanie w tej odpowiedzi SO działa (choć nie tak eleganckie jak powyżej). Jeśli to błąd, czy ktoś to zgłosił?
Jus12
2
NAPRAW: Zakres zamówienia nie jest wciągany, możesz go niejawnie wciągnąć, ale dość łatwo jest po prostu użyć bezpośrednio kolejności: def compare (that: A) = Ordering.Tuple2 [String, String] .compare (tuple (this ), tuple (that))
brendon
46
object A {
  implicit val ord = Ordering.by(unapply)
}

Ma to tę zaletę, że jest aktualizowane automatycznie po każdej zmianie A. Ale pola A muszą być ułożone w kolejności, w jakiej zamawiający je wykorzysta.

IttayD
źródło
Wygląda fajnie, ale nie potrafię wymyślić, jak tego użyć, otrzymuję:<console>:12: error: not found: value unapply
zbstof
29

Podsumowując, można to zrobić na trzy sposoby:

  1. Do jednorazowego sortowania użyj metody .sortBy, jak pokazał @Shadowlands
  2. Do ponownego użycia sortowania rozszerz klasę przypadku z cechą Ordered, jak powiedział @Keith.
  3. Zdefiniuj zamówienie niestandardowe. Zaletą tego rozwiązania jest to, że możesz ponownie używać kolejności i mieć wiele sposobów sortowania wystąpień tej samej klasy:

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

Odpowiadając na twoje pytanie Czy jest jakaś standardowa funkcja zawarta w Scali, która potrafi magicznie, jak List ((2,1), (1,2)). Sortowane

Istnieje zestaw predefiniowanych kolejności , np. Dla String, krotek do 9 ar, i tak dalej.

Nie ma czegoś takiego dla klas przypadków, ponieważ nie jest łatwo wycofać się, biorąc pod uwagę, że nazwy pól nie są znane a-priori (przynajmniej bez magii makr) i nie można uzyskać dostępu do pól klas przypadków w inny sposób niż przez nazwa / używanie iteratora produktu.

mniam mniam mniam
źródło
Bardzo dziękuję za przykłady. Spróbuję zrozumieć ukrytą kolejność.
ya_pulser
8

unapplyMetoda obiektu towarzyszącego zapewnia konwersję z klasy sprawę do Option[Tuple], gdzie Tuplejest krotka odpowiadający pierwszej listy argumentów klasy sprawy. Innymi słowy:

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

def sortPeople(people : List[Person]) = 
    people.sortBy(Person.unapply)
Joakim Ahnfelt-Rønne
źródło
6

Metoda sortBy byłaby typowym sposobem wykonania tego, np. (Sortowanie według tagpola):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)
Shadowlands
źródło
Co zrobić w przypadku 3+ pól w przypadku klasy? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser
Jeśli używasz sortBy, to tak, albo to, albo dodaj / użyj odpowiedniej funkcji do / na klasie (np. _.toStringLub własnej niestandardowej metody lub funkcji zewnętrznej o znaczeniu leksograficznym).
Shadowlands
Czy jest jakaś standardowa funkcja zawarta w Scali, która może robić magię, podobnie jak List((2,1),(1,2)).sortedobiekty klasy przypadku? Nie widzę dużej różnicy między nazwanymi krotkami (klasa przypadku == nazwana krotka) a prostymi krotkami.
ya_pulser
Najbliższe, co mogę zrobić po tych liniach, to użycie metody unapply obiektu towarzyszącego, aby uzyskać Option[TupleN]znak, a następnie wywołanie gettego:l.sortBy(A.unapply(_).get)foreach(println) które używa podanej kolejności w odpowiedniej krotce, ale jest to po prostu wyraźny przykład ogólnej idei, którą podałem powyżej .
Shadowlands
5

Ponieważ użyłeś klasy case, którą możesz rozszerzyć za pomocą Ordered w następujący sposób :

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
źródło