一个简单的case class 如何定义易于理解的排序方式?

119

我有一个简单的Scala case class实例列表,想要使用list.sorted按可预测的词典顺序打印它们,但是收到了“没有为...定义隐式排序”的错误信息。

是否存在提供case class词典顺序的隐式对象?

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

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
我刚刚写了一篇博客文章,介绍了几种通用解决方案,链接在这里:http://meta.plasm.us/posts/2013/10/13/ordering-case-classes/ 。 - Travis Brown
7个回答

173

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

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接口的任何类都处于作用域内。对于Tuple,存在隐式的Ordering,如果所有元素T1, ..., TN都存在隐式的Ordering[TN],则可以将TupleN[...]转换为Ordered[TupleN[...]],这应该总是成立的,因为在没有Ordering的数据类型上进行排序是没有意义的。

对于涉及复合排序键的任何排序场景,隐式排序对于Tuples是您的首选:

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 的实例,因此直接提供排序可以在大多数情况下消除隐式转换。

1
你的例子很棒!一行代码就能实现默认排序。非常感谢你。 - ya_pulser
7
答案中建议使用的案例类 A 在 Scala 2.10 下似乎无法编译。我是否漏掉了什么? - Doron Yaacoby
3
@DoronYaacoby 发生了一个错误: value compare is not a member of (String, Int) - bluenote10
1
@JCracknell 即使导入(Scala 2.10.4),错误仍然存在。错误发生在编译期间,但在IDE中未被标记(有趣的是,在REPL中它确实可以正常工作)。对于那些遇到这个问题的人,此SO答案中的解决方案有效(虽然不如上面的优雅)。如果这是一个错误,有人报告了吗? - Jus12
2
修复:Ordering 的范围没有被拉入,你可以隐式地将其拉入,但直接使用 Ordering 更容易: def compare(that: A) = Ordering.Tuple2[String, String].compare(tuple(this), tuple(that)) - brendon
显示剩余9条评论

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

这样做的好处是,每当A发生变化时,它会自动更新。但是,A的字段需要按照排序将使用它们的顺序进行排序。


1
看起来很酷,但我无法弄清如何使用它,我得到了:<console>:12: error: not found: value unapply - zbstof

29

总结一下,有三种方法可以实现这个:

  1. 如果只需要进行一次排序,可以使用 .sortBy 方法,就像 @Shadowlands 所展示的那样。
  2. 如果需要重复使用排序功能,可以将 case class 与 Ordered trait 相结合,就像 @Keith 所说的那样。
  3. 自定义排序方式。这种解决方案的好处是可以重复使用排序,并且可以有多种方法对同一类的实例进行排序:

  4. 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))
    
    Answering your question Scala是否包含类似于List((2,1),(1,2)).sorted这样的函数魔法的标准函数? 有一组预定义的排序规则,例如String,最多可以进行9种排序等等。
    对于case classes并不存在这样的东西,因为它不容易实现,给定字段名在没有宏的情况下事先是未知的,并且您无法以名称以外的方式访问case class字段/使用product iterator。

非常感谢您提供的示例。我会努力理解隐式排序。 - ya_pulser

8

伴生对象的unapply方法提供了一种将您的case class转换为Option[Tuple]的方法,其中Tuple是与case class的第一个参数列表对应的元组。换句话说:

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

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

如果有人在使用Scala 3,请检查方法Tuple.fromProductTyped。我刚刚花了两个小时弄清楚为什么这个答案中的代码在Scala 3中无法编译。看起来现在默认生成的unapply提取器是一个身份函数。 - Dawid Łakomy

7

sortBy方法是实现此目的的一种典型方式,例如(按tag字段排序):

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

如果在case类中有3个或更多字段,应该怎么办?l.sortBy( e => e._tag + " " + e._load + " " + ... ) ? - ya_pulser
如果使用 sortBy,那么是的,要么这样做,要么在类上添加/使用适当的函数(例如 _.toString,或者您自己的按字典顺序重要的自定义方法或外部函数)。 - Shadowlands
Scala中是否包含任何标准函数,可以像List((2,1),(1,2)).sorted一样对案例类对象进行排序?我认为命名元组(case class == named tuple)和简单元组之间没有太大的区别。 - ya_pulser
我能想到的最接近的方法是使用伴生对象的unapply方法获取一个Option[TupleN],然后在其上调用getl.sortBy(A.unapply(_).get)foreach(println),它使用相应元组上提供的排序方式,但这只是我上面提到的一般思路的一个明确示例。 - Shadowlands

5

由于您使用了一个case类,因此可以像这样扩展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

0

我个人最喜欢的方法是使用SAM(单一抽象方法),如下面的例子所述,版本为2.12:

case class Team(city:String, mascot:String)

//Create two choices to sort by, city and mascot
object MyPredef3 {
  // Below used in 2.11
  implicit val teamsSortedByCity: Ordering[Team] = new Ordering[Team] {
    override def compare(x: Team, y: Team) = x.city compare y.city
  }

  implicit val teamsSortedByMascot: Ordering[Team] = new Ordering[Team] {
    override def compare(x: Team, y: Team) = x.mascot compare y.mascot
  }

  /*
     Below used in 2.12
     implicit val teamsSortedByCity: Ordering[Team] =
    (x: Team, y: Team) => x.city compare y.city
     implicit val teamsSortedByMascot: Ordering[Team] =
    (x: Team, y: Team) => x.mascot compare y.mascot

   */
}

object _6OrderingAList extends App {
  //Create some sports teams
  val teams = List(Team("Cincinnati", "Bengals"),
    Team("Madrid", "Real Madrid"),
    Team("Las Vegas", "Golden Knights"),
    Team("Houston", "Astros"),
    Team("Cleveland", "Cavaliers"),
    Team("Arizona", "Diamondbacks"))

  //import the implicit rule we want, in this case city
  import MyPredef3.teamsSortedByCity

  //min finds the minimum, since we are sorting
  //by city, Arizona wins.
  println(teams.min.city)

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接