为简单案例类定义订购的简单惯用方式


110

我有一个简单的scala案例类实例列表,我想使用list.sorted,以可预测的字典顺序打印它们,但是会收到“没有为...定义隐式排序”。

是否存在为案例类提供词典编排顺序的隐式?

是否有简单的惯用方式将词典顺序混合到案例类中?

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

我对“ hack”不满意:

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

8
我刚刚写了一篇博客文章一对夫妇通用的解决方案在这里
特拉维斯·布朗

Answers:


152

我个人最喜欢的方法是使用提供的元组隐式排序,因为它是清晰,简洁和正确的:

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

之所以有效,是因为的伴随Ordered定义了从Ordering[T]到的隐式转换Ordered[T]任何实现的类都属于范围Ordered。隐的存在OrderingS表示TupleS启用从一个转换TupleN[...]Ordered[TupleN[...]]提供的隐式Ordering[TN]存在的所有元素T1, ..., TN的元组的,这应始终是因为它是没有意义的排序上没有一个数据类型的情况下Ordering

对于任何涉及复合排序键的排序方案,元组的隐式排序都是您的首选:

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

由于这个答案很受欢迎,因此我想对此进行扩展,并指出在某些情况下可以将类似于以下内容的解决方案视为企业级™:

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

给定es: SeqLike[Employee]es.sorted()将按名称排序,并将es.sorted(Employee.orderingById)按ID排序。这有一些好处:

  • 排序在单个位置定义为可见的代码工件。如果您在许多字段上进行复杂的排序,这将很有用。
  • Scala库中实现的大多数排序功能都是使用的实例运行的Ordering,因此在大多数情况下,提供排序可以直接消除隐式转换。

您的例子很棒!单线和我有默认订购。非常感谢你。
ya_pulser 2013年

7
答案中建议的案例类A似乎未在scala 2.10下编译。我想念什么吗?
Doron Yaacoby 2014年

3
@DoronYaacoby:我也遇到错误value compare is not a member of (String, Int)
bluenote10 2014年

1
@JCracknell即使导入(Scala 2.10.4)后,错误仍然存​​在。编译期间发生错误,但未在IDE中进行标记。(有趣的是,它确实可以在REPL中正常工作)。对于那些有此问题的人,此SO答案的解决方案有效(尽管不如上面所述那么优雅)。如果是错误,是否有人报告过?
2014年

2
FIX:Ordering的范围不会被拉进去,您可以隐式地将它拉进去,但是直接直接使用Ordering就足够了:def compare(that:A)= Ordering.Tuple2 [String,String] .compare(tuple(this ),tuple(that))
brendon

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

这样的好处是,只要A更改,它就会自动更新。但是,A的字段需要按照顺序使用它们的顺序放置。


看起来很酷,但是我不知道该如何使用,我得到:<console>:12: error: not found: value unapply
zbstof19年

29

总结起来,有三种方法可以做到这一点:

  1. 对于一次性排序,请使用.sortBy方法,如@Shadowlands所示
  2. 为了重用排序,请扩展具有Ordered特性的case类,如@Keith所述。
  3. 定义自定义顺序。该解决方案的好处在于,您可以重用顺序并具有多种方法来对同一类的实例进行排序:

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

回答您的问题Scala中是否包含任何可以执行魔术的标准函数,例如List((2,1),(1,2,))。sorted

有一组预定义的顺序,例如,用于String,元组(最多9个arity)等等。

对于案例类,不存在这样的事情,因为要转换字段名并不是先验的(至少在没有使用魔术的情况下),并且您不能以除by之外的其他方式访问案例类字段,因此要滚降是不容易的事情名称/使用产品迭代器。


非常感谢您提供示例。我将尝试了解隐式排序。
ya_pulser 2013年

8

unapply伴随对象的方法提供了从您的case类到的转换Option[Tuple],其中Tuple是与case类的第一个参数列表相对应的元组。换一种说法:

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

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

6

sortBy方法将是一种典型的方法,例如(按tag字段排序):

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

如果案例类中有3个以上字段,该怎么办?l.sortBy( e => e._tag + " " + e._load + " " + ... )
ya_pulser

如果使用sortBy,则选择是,或者在该类上/在该类上添加/使用一个合适的函数(例如_.toString,或您自己的具有字法学意义的自定义方法或外部函数)。
Shadowlands

Scala中包含任何标准功能,可以像List((2,1),(1,2)).sorted案例类对象一样神奇吗?我发现命名元组(案例类==命名元组)和简单元组之间没有太大区别。
ya_pulser

我能理解的最接近的是使用伴随对象的unapply方法来获取Option[TupleN],然后对其进行调用getl.sortBy(A.unapply(_).get)foreach(println),它在相应的元组上使用提供的顺序,但这只是我上面给出的一般思想的一个明确示例。 。
Shadowlands

5

由于使用了案例类,因此可以像这样扩展Ordered

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.