6

I would like to subtract two consecutive element in a list with numbers in Scala.

For example : I have this list :

val sortedList = List(4,5,6)

I would like to have an output list like diffList =(1, 1) where 5-4 = 1 and 6-5 = 1.

I tried the following code:

var sortedList = List[Int]()
var diffList = List[Int]()

for (i <- 0 to (sortedList.length - 1) ;j <- i + 1 to sortedList.length - 1) 
{
    val diff = (sortedList(j) - sortedList(i))
    diffList = diffList :+ diff
}

I have the following result for diffList =(1, 2, 1) but I want diffList = (1,1).

It's because of the for loop. it does not iterate over the two variables (i and j) at once.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
Sandra
  • 85
  • 6

4 Answers4

9

You do not mutability nor imperative programming to solve this problem, functional programming got you covered.

def consecutiveDifferences(data: List[Int]): List[Int] =
  if (data.isEmpty) List.empty
  else data.lazyZip(data.tail).map {
    case (x, y) => y - x
 }

As I always say, the Scaladoc is your friend.
(Also, as an advice, the best way to learn functional programming is to forbid yourself from mutability)

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • Thank you. The logic is very different from java programming. – Sandra Jan 30 '21 at 19:54
  • 1
    @Sandra yeah learning FP takes some time, especially because you need to unlearn your habits from imperative programming. That is why my advice is to force yourself to not using mutability and to use the **Scaladoc** in your advantage. – Luis Miguel Mejía Suárez Jan 30 '21 at 20:13
7

You can use the sliding method, which according to the docs:

/** Groups elements in fixed size blocks by passing a "sliding window"
 *  over them (as opposed to partitioning them, as is done in `grouped`.)
 *
 *  An empty collection returns an empty iterator, and a non-empty
 *  collection containing fewer elements than the window size returns
 *  an iterator that will produce the original collection as its only
 *  element.
 *  @see [[scala.collection.Iterator]], method `sliding`
 *
 *  @param size the number of elements per group
 *  @return An iterator producing ${coll}s of size `size`, except for a
 *          non-empty collection with less than `size` elements, which
 *          returns an iterator that produces the source collection itself
 *          as its only element.
 *  @example `List().sliding(2) = empty iterator`
 *  @example `List(1).sliding(2) = Iterator(List(1))`
 *  @example `List(1, 2).sliding(2) = Iterator(List(1, 2))`
 *  @example `List(1, 2, 3).sliding(2) = Iterator(List(1, 2), List(2, 3))`
 */

Then, solving your query is pretty straight forward:

diffList = sortedList.sliding(2).collect {
  case Seq(a, b) =>
    b - a
}.toList

Which results in List(1,1)

Code run at Scastie.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • 1
    Very nice @Tomer Shetah – Zvi Mints Jan 30 '21 at 20:59
  • That's a non-exhaustive pattern match and thus a bad idea. – Matthias Berndt Jan 30 '21 at 21:34
  • 2
    @MatthiasBerndt, the usage of `collect`, is like `filter` + `map`. Therefore usually when using `collect` you do not exauhst the pattern matching. If `diffList` has less then 2 elements, there will be no match, and the result will be empty list as expected. Otherwise all elements will be matched. – Tomer Shetah Jan 30 '21 at 21:39
  • @MatthiasBerndt, please read https://stackoverflow.com/a/4784562/2359227 – Tomer Shetah Jan 30 '21 at 21:40
  • @MatthiasBerndt, First, please note that the return type of the `sliding` method is `Iterator`. The reason I added `toList` at the end is to show the correctness of my code. Similarly you can use `toSeq` or any other materialization that you would like. Second, what we would like to filter, is lists of size 0, and 1, which can happen in case `sortedList` has less then 2 elements. In such case we cannot compute the differences, and we want to get empty list as a result. Using `map` will cause runtime exception. Please see: https://scastie.scala-lang.org/toshetah/eiR2ymg0SoWad8JTgnBQSw/4 – Tomer Shetah Jan 31 '21 at 06:28
  • While collect will provide the desired output: https://scastie.scala-lang.org/toshetah/eiR2ymg0SoWad8JTgnBQSw/5 – Tomer Shetah Jan 31 '21 at 06:39
  • @MatthiasBerndt So just collect with `Seq` instead... – Zvi Mints Jan 31 '21 at 09:46
  • Sorry, I meant if you change the type of `sortedList` to `Vector` it will break. – Matthias Berndt Jan 31 '21 at 09:46
  • 1
    Besides, it's still hacky to first call a method that will produce elements of length 2 that you then filter out again. It's better to not produce such elements in the first place by not using the inappropriate `sliding` method. Just do `sortedList.zip(sortedList.drop(1))`. Zvi Mints' solution is clearly the better one. – Matthias Berndt Jan 31 '21 at 09:48
  • 3
    @MatthiasBerndt you still miss the point. The only reason to filter, is for short (< 2) lists. Otherwise it won't be filtered, it will be computed. It is fine that you think that Zvi's solution is better, but I still don't understand why to downvote this legit solution. – Tomer Shetah Jan 31 '21 at 09:59
  • 1
    @MatthiasBerndt BTW, if you wanted to increase the sliding window, for example to subtract the 2 following elements from the first one, it will be harder using `zip`, and easy with `sliding`. Not to talk about 10 elements. – Tomer Shetah Jan 31 '21 at 10:23
  • I understand perfectly well that you filter out short lists with collect. I still think it's fragile because it breaks when you change the type of `sortedType` to `Seq`, and I think that's a valid reason to downvote. – Matthias Berndt Jan 31 '21 at 10:37
  • And if you want to subtract the following 10 numbers from every element in the list, then using `collect` in this manner is basically as unwieldy as using `zip` in a naive manner. A good solution for that case would be `sortedList.zip(sortedList.drop(1).sliding(10)).map(Function.tupled(_ - _.sum))` (though not ideal from a performance pov) – Matthias Berndt Jan 31 '21 at 10:44
  • Technically, if you just explicitly run `.map(_.toList)` or other specific collection before `.collect` (or maybe matching on `case Seq(x, y)`), would be perfectly safe. Also, `.collect` might be even redundant, as e.g. `.sliding(2)` will produce _only_ sequences of 2 elements, List of one, or empty list won't happen. The only reason you have a List here is because you would have to have some dependent types to match on tuples (param = 2 =>Tuple2, param = 3 => Tuple3, etc) and that would be annoying to use. – Mateusz Kubuszok Feb 01 '21 at 14:00
  • 1
    Thanks @MateuszKubuszok for the input. I changed as you suggested `List` to `Seq` in the `collect` clause. I totally agree with the second part of your comment :) – Tomer Shetah Feb 01 '21 at 14:25
5
for(i <- 0 until (sortedList.size - 1)) yield sortedList(i + 1) - sortedList(i)

yield Vector(1,1) which can be converted to list with toList

That's can be also achieved with the following function:

  val sortedList = List(4,5,7)
  @tailrec
  def findDiffs(xs: List[Int])(seed: List[Int]): List[Int] = {
    if(xs.isEmpty || xs.size == 1) seed.reverse
    else {
      val currDiff = xs(1) - xs(0)
      findDiffs(xs.tail)(currDiff :: seed)
    }
  }
  val res = findDiffs(sortedList)(Nil)
  println(res)

Or just easily with zip:

sortedList.drop(1) zip sortedList map { case (x,y) => x - y } 
Zvi Mints
  • 1,072
  • 1
  • 8
  • 20
0

Sliding (see answer by @Tomer Shetah) over a list delivers an iterator, which may prove convenient for very large collections to avoid/reduce the amount of intermediate structures in the processing. Another approach includes the zipping of the list with itself shifted by one (see answers by @Luis Miguel Mejía Suárez and @Zvi Mints); in this regard another approach to shifting and then zipping is by dropping the first element as in

xs.drop(1) zip xs map {case(a,b) => b-a}

This can be generalised by dropping any number n so that we subtract the first and the n-th elements, then the second and the n+1-th elements, and so forth.

iris
  • 379
  • 1
  • 2
  • 9