Every now and then I need to use map on a collection in a way that depends on what has been previously observed of the collection. This could be handled with a var outside of the mapping block that gets updated from within the block. I’d rather avoid those vars in regular, non-library code, though. It can also be done recursively, but that doesn’t look like map anymore. A simple solution is
implicit class StatefulMapper[T](list: List[T]) { // Anything that can be mapped?
def withStateMap[S, U](initialState: S)(f: (S, T) => (S, U)): List[U] = {
var currentState = initialState
list.map { value: T =>
val (newState: S, newValue: U) = f(currentState, value)
currentState = newState
newValue
}
}
}
List(1, 2, 3, 4, 5, 7, 8).withStateMap(false) { case (oddRun, value) =>
val newOddRun = value % 2 == 1
(newOddRun, if (oddRun) -value else value)
} // List(1, -2, 3, -4, 5, -7, -8)
This works for a List, of course. What is the best way to write it so that it works on just about anything that can be mapped, though? Or is there a completely different and better way to do it? Sometimes a similar foreach would be handy.
Fold seems to result in a single value rather than a collection based on the original collection. Here it would be a U while I’m hoping for a List[U]. I could turn it around and use the list as the state and return the last state instead of the result from the fold, but that doesn’t seem quite right.
U can be List[U] that is not an issue.
Folding is the natural transformation of collections, it is the more general and flexible combinator that abstracts the concept of traversal (e.g. recursion). You can implement all operations in List using only foldLeft
Cool! There are a few lots of things in there I would never have thought of without first diving deep into the implementation of the standard collections, like IsIterableOnce, BuildFrom, and toFactory and maybe even the null and drop. Writing code like this to get it properly integrated isn’t for mere mortals, I think. In the end, does this iterate through the collection just once despite both the scan and map?
Thanks for the tip. Most of the time what I’m working on doesn’t have an existing dependency on cats so that using mapAccumulate would be nearly free, but someday that may change. I’ll watch for it being added to the scala library and then being backported to scala-collection-compat.
I remember when I started to learn Scala, I really didn’t want to add cats as a dependency, yet almost every time I had a question I found an answer using cats. Until one day I said “no more” and just added it, best decision ever.
There is really no reason not to have cats in scope, is relatively small, and is just a bunch of utility methods (you can think of it as an extended stdlib), doesn’t imply any paradigm shift like cats-effect.
No doubt, but can isn’t the same as should.
Well, again, using List[U] as the accumulator of a fold is not only possible, is okay. There is no rule that says that you shouldn’t do that.
But yes, scanLeft may be a better fit in this case, I forgot that one was part of the stdlib.
Iterator.unfold(false -> List(1,2,3,4,5,7,8)){
case (oddRun, values) =>
values match {
case Nil => None
case value :: rest =>
val newOddRun = value % 2 == 1
val a = if (oddRun) -value else value
val s = (newOddRun, rest)
Option(a -> s)
}
}.toList
// List(1, -2, 3, -4, 5, -7, -8)
That’s cool. I have to admit not being up to date with Scala 3 possibilities. It is nice to know when one sees things like map, that the incoming and outgoing collections are of the same size. Here I would have to inspect the code to make sure. Can’t have everything, I guess.