Maneira idiomática fácil de definir pedidos para uma classe de caso simples

110

Eu tenho uma lista de instâncias de classe de caso scala simples e quero imprimi-las em ordem lexicográfica previsível usando list.sorted, mas recebo "Nenhum pedido implícito definido para ...".

Existe um implícito que fornece ordenação lexicográfica para classes de caso?

Existe uma maneira idiomática simples de misturar a ordenação lexicográfica na classe 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)
            ^

Não estou feliz com um 'hack':

scala> l.map(_.toString).sorted.foreach(println)
A(article,2)
A(lines,7)
A(words,50)
ya_pulser
fonte
8
Acabei de escrever uma postagem no blog com algumas soluções genéricas aqui .
Travis Brown

Respostas:

152

Meu método favorito é fazer uso da ordenação implícita fornecida para tuplas, pois é claro, conciso e correto:

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

Isso funciona porque o companheiro paraOrdered define uma conversão implícita de Ordering[T]para Ordered[T]que está no escopo de qualquer implementação de classe Ordered. A existência de Orderings implícitos para Tuples permite uma conversão de TupleN[...]em, Ordered[TupleN[...]]desde que Ordering[TN]exista um implícito para todos os elementos T1, ..., TNda tupla, o que sempre deve ser o caso, pois não faz sentido classificar por um tipo de dados com no Ordering.

A ordenação implícita para Tuplas é a sua escolha para qualquer cenário de classificação que envolva uma chave de classificação composta:

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

Como esta resposta se provou popular, gostaria de expandi-la, observando que uma solução semelhante à seguinte poderia, em algumas circunstâncias, ser considerada de nível 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()irá classificar por nome e es.sorted(Employee.orderingById)irá classificar por id. Isso tem alguns benefícios:

  • As classificações são definidas em um único local como artefatos de código visíveis. Isso é útil se você tiver classificações complexas em muitos campos.
  • A maioria das funcionalidades de classificação implementadas na biblioteca scala opera usando instâncias de Ordering, portanto, fornecer uma classificação elimina diretamente uma conversão implícita na maioria dos casos.
J Cracknell
fonte
Seu exemplo é maravilhoso! Linha única e tenho ordenação padrão. Muito obrigado.
ya_pulser
7
A classe de caso A sugerida na resposta não parece compilar no scala 2.10. Estou esquecendo de algo?
Doron Yaacoby
3
@DoronYaacoby: Também recebo um erro value compare is not a member of (String, Int).
bluenote10
1
@JCracknell O erro ainda está lá mesmo após a importação (Scala 2.10.4). O erro ocorre durante a compilação, mas não é sinalizado no IDE. (curiosamente, ele funciona corretamente no REPL). Para aqueles que têm esse problema, a solução nesta resposta SO funciona (embora não seja tão elegante quanto a acima). Se for um bug, alguém já relatou?
Jus12
2
CORREÇÃO: O escopo do Ordering não está sendo puxado, você pode puxá-lo implicitamente, mas é fácil o suficiente apenas usar o Ordering diretamente: def compare (that: A) = Ordering.Tuple2 [String, String] .compare (tuple (this ), tupla (que))
brendon
46
object A {
  implicit val ord = Ordering.by(unapply)
}

Isso tem a vantagem de ser atualizado automaticamente sempre que A muda. Mas, os campos de A precisam ser colocados na ordem em que o pedido os usará.

IttayD
fonte
Parece legal, mas não consigo descobrir como usar isso, recebo:<console>:12: error: not found: value unapply
zbstof
29

Para resumir, existem três maneiras de fazer isso:

  1. Para classificação única, use o método .sortBy, como @Shadowlands mostrou
  2. Para reutilizar a classificação estenda a classe de caso com o traço Ordenado, como disse @Keith.
  3. Defina um pedido personalizado. O benefício desta solução é que você pode reutilizar ordens e ter várias maneiras de classificar instâncias da mesma 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))

Respondendo à sua pergunta Existe alguma função padrão incluída no Scala que pode fazer mágica como List ((2,1), (1,2)).

Há um conjunto de ordenações predefinidas , por exemplo, para String, tuplas até 9 aridade e assim por diante.

Não existe tal coisa para classes de caso, uma vez que não é uma coisa fácil de rolar, dado que os nomes dos campos não são conhecidos a priori (pelo menos sem a magia das macros) e você não pode acessar os campos da classe de caso de outra forma que não nome / usando o iterador do produto.

om-nom-nom
fonte
Muito obrigado pelos exemplos. Tentarei entender a ordenação implícita.
ya_pulser
8

O unapplymétodo do objeto complementar fornece uma conversão de sua classe de caso para um Option[Tuple], em que Tupleé a tupla correspondente à primeira lista de argumentos da classe de caso. Em outras palavras:

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

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

O método sortBy seria uma maneira típica de fazer isso, por exemplo (classificar no tagcampo):

scala> l.sortBy(_.tag)foreach(println)
A(article,2)
A(lines,7)
A(words,50)
Shadowlands
fonte
O que fazer no caso de 3+ campos na classe de caso? l.sortBy( e => e._tag + " " + e._load + " " + ... )?
ya_pulser
Se estiver usando sortBy, então sim, ou adicione / use uma função adequada para / na classe (por exemplo _.toString, ou seu próprio método personalizado lexograficamente significativo ou função externa).
Shadowlands
Existe alguma função padrão incluída no Scala que pode fazer mágica List((2,1),(1,2)).sortedpara os objetos da classe de caso? Não vejo grande diferença entre tuplas nomeadas (classe de caso == tupla nomeada) e tuplas simples.
ya_pulser
O mais próximo que pode obter ao longo destas linhas é usar o método cancelar a aplicação do objeto companheiro para obter um Option[TupleN], em seguida, chamar getem que: l.sortBy(A.unapply(_).get)foreach(println), que usa a ordenação fornecido na tupla correspondente, mas este é simplesmente um exemplo explícito da idéia geral eu dou acima .
Shadowlands
5

Já que você usou uma classe de caso, você pode estender com Ordered assim:

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
fonte