How to sum fields of collection elements without mapping them first (like foldLeft/reduceLeft)?
Asked Answered
B

3

14

Consider this class:

 case class Person(val firstName: String, val lastName: String, age: Int)
 val persons = Person("Jane", "Doe", 42) :: Person("John", "Doe", 45) :: 
               Person("Joe", "Doe", 43) :: Person("Doug", "Don", 65) :: 
               Person("Darius", "Don", 24) :: Person("Dora", "Don", 20) :: 
               Person("Dane", "Dons", 29) :: Nil

To get the sum of the age of all persons, I can write code like:

persons.foldLeft(0)(_ + _.age)

But if I want to use sum, I need to map the value first and the code looks like this:

persons.map(_.age).sum

How can I use the sum method without creating some intermediate collection?

(I know that such an "optimization" most probably doesn't have any real performance difference when not run in a tight loop and I also know about lazy views and so on.)

Is it possible to have code like

persons.sum(_.age)

doing what foldLeft/reduceLeft does?

Byte answered 21/2, 2011 at 9:45 Comment(6)
I guess sumBy (cf. sortBy) would be a reasonable extension for the collection library.Carouse
That's what I was thinking too. Although I can't really decide between a) Overloading sum (many devs don't like overloading) b) sumBy (consistent with SortBy) and c) sumOf (nicer to read, like "totalAge is defined as persons sumOf age").Byte
Ugh, library extension? Next we'll need prodOf and maxOf and minOf. And then those of us who like median or mean or entropy or (etc.) will feel left out. A good old mapReduce might be nice. But beyond that I think it's better for all involved to get used to folding and/or views.Hostel
@Rex Kerr: Uh? We already have maxBy and minBy! That's the point I am making here.Byte
@Byte - Argh. Well, it's not in 2.8.1. And I think it's foolish to do for 2.9.Hostel
Why? Following these arguments, we could remove everything except filter, flatten and map, because those are just some "convenenience" method.Byte
L
11

The method sum in the library doesn't work this way, but you could write your own which does:

def mySum[T, Res](f: T => Res, seq: TraversableOnce[T])(implicit num: Numeric[Res]) = 
  seq.foldLeft(num.zero)((acc, b) => num.plus(acc, f(b)))

You could also add an implicit conversion so you can call it like seq.sum(f) instead of mySum(f, seq) (you may need a different name than sum to avoid conflicts):

case class SumTraversableOnce[T](val seq: TraversableOnce[T]) { 
  def sum[Res](f: T => Res)(implicit num: Numeric[Res]) = mySum(f, seq)(num) 
}

implicit def toSumTraversableOnce[T](seq: TraversableOnce[T]) = 
  SumTraversableOnce(seq)

or, since Scala 2.10,

implicit class SumTraversableOnce[T](val seq: TraversableOnce[T]) { 
  def sum[Res](f: T => Res)(implicit num: Numeric[Res]) = mySum(f, seq)(num) 
}
Lackadaisical answered 21/2, 2011 at 10:52 Comment(1)
I can't get it to compile here (2.9 trunk). I think 0 should be num.zero.Byte
I
13

You answered is yourself. Just use view:

persons.view.map(_.age).sum

To convince yourself by examining the workflow:

persons.view.map { p =>
  println("invoking age")
  p.age
}.map { x =>
  println("modifing age")
  x + 0
}.sum

Vs:

persons.map { p =>
  println("invoking age")
  p.age
}.map { x =>
  println("modifing age")
  x + 0
}.sum
Inclusive answered 21/2, 2011 at 11:13 Comment(5)
Well, view was exactly the thing i don't wanted to use. :-)Byte
May I ask why you exclude view?Inclusive
I wondered if I did miss a method which fills the gap between sum and foldLeft.Byte
You didn't miss anything. sum is more or less your foldLeft: def sum[B >: A](implicit num: Numeric[B]): B = foldLeft(num.zero)(num.plus) (from Scala source). I don't know a to make something in-between without adding indirection.Inclusive
Looks like someone just filed a ticket about it: lampsvn.epfl.ch/trac/scala/ticket/4276Byte
L
11

The method sum in the library doesn't work this way, but you could write your own which does:

def mySum[T, Res](f: T => Res, seq: TraversableOnce[T])(implicit num: Numeric[Res]) = 
  seq.foldLeft(num.zero)((acc, b) => num.plus(acc, f(b)))

You could also add an implicit conversion so you can call it like seq.sum(f) instead of mySum(f, seq) (you may need a different name than sum to avoid conflicts):

case class SumTraversableOnce[T](val seq: TraversableOnce[T]) { 
  def sum[Res](f: T => Res)(implicit num: Numeric[Res]) = mySum(f, seq)(num) 
}

implicit def toSumTraversableOnce[T](seq: TraversableOnce[T]) = 
  SumTraversableOnce(seq)

or, since Scala 2.10,

implicit class SumTraversableOnce[T](val seq: TraversableOnce[T]) { 
  def sum[Res](f: T => Res)(implicit num: Numeric[Res]) = mySum(f, seq)(num) 
}
Lackadaisical answered 21/2, 2011 at 10:52 Comment(1)
I can't get it to compile here (2.9 trunk). I think 0 should be num.zero.Byte
A
0

In Scala 3 you can create the following extension:

extension [T](iterable: Iterable[T])
  def sumBy[U](f: T => U)(using n: Numeric[U]): U = iterable.foldLeft(n.zero)((acc, elem) => n.plus(acc, f(elem)))
Amritsar answered 10/1, 2023 at 15:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.