2

In my code, I very often need to process a list by performing operations on an internal model. For each processed element, the model is returned and then a 'new' model is used for the next element of the list.

Usually, I implement this by using a tail recursive method:

def createCar(myModel: Model, record: Record[Any]): Either[CarError, Model] = {
  record match {
    case c: Car =>
      // Do car stuff...
      val newModel: Model = myModel.createCar(record)
      Right(newModel)
    case _ => Left(CarError())
  }
}

@tailrec
def processCars(myModel: Model, records: List[Record[Any]]): Either[CarError, Model] =
  records match {
    case x :: xs =>
      createCar(myModel, x) match {
        case Right(m) => processCars(m, xs)
        case e@Left(_) => e
      }
    case Nil => Right(myModel)
  }

Since I keep repeating this kind of pattern, I am searching for ways to make it more concise and more functional (i.e., the Scala way). I have looked into foldLeft, but cannot get it to work with Either:

recordsList.foldLeft(myModel) { (m, r) =>
      // Do car stuff...           
      Right(m)
}

Is foldLeft a proper replacement? How can I get it to work?

John Doe
  • 113
  • 1
  • 2
  • 11
  • 2
    `foldLeft` does seem to be the correct way to do this if you are in vanilla Scala. Fold has the signature `(z: B)(op: (B, A) ⇒ B): B`, so if the function you pass returns an instance of `Either`, your initial element has to be of the same type i.e. `Right(myModel)` – sinanspd Feb 18 '21 at 20:31
  • 1
    However note that `foldLeft` ***doens't*** allow you to stop early/short-circuit. Even if the first element in your List returns an error, you have to keep iterating through the list. If you really want to keep the short circuit behavior you need to use recursion or use `cats` and `foldM` – sinanspd Feb 18 '21 at 20:33
  • Maybe the best would be to keep your tai-recursive algorism but abstract it with functions so you can reuse it in multiple places with minimum boilerplate? – Luis Miguel Mejía Suárez Feb 18 '21 at 20:40
  • 1
    `unfold()` will allow early termination, i.e. on 1st `Left`, but it does build a `List` of results, of which the last element is the only one you're interested in. – jwvh Feb 18 '21 at 20:41

2 Answers2

4

Following up on my earlier comment, here's how to unfold() to get your result. [Note: Scala 2.13.x]

def processCars(myModel: Model
               ,records: List[Record[_]]
               ): Either[CarError, Model] =
  LazyList.unfold((myModel,records)) { case (mdl,recs) =>
    recs.headOption.map{
      createCar(mdl, _).fold(Left(_) -> (mdl,Nil)
                            ,m => Right(m) -> (m,recs.tail))
    }
  }.last

The advantage here is:

  1. early termination - Iterating through the records stops after the 1st Left is returned or after all the records have been processed, whichever comes first.
  2. memory efficient - Since we're building a LazyList, and nothing is holding on to the head of the resulting list, every element except the last should be immediately released for garbage collection.
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • Thanks a lot! Is `myModel` always the same instance from the initial method call `processCars` or is it always the "updated" instance as we iterate through the `LazyList`? – John Doe Feb 19 '21 at 08:56
  • 1
    The posted code for `createCar()` always returns the received value for `myModel` when it returns `Right`. In fact, neither method allows for any changes to `myModel`. My proposed solution rather slavishly imitates that behavior. This can be fixed. Does the real-world `createCar()` return something different than the received `myModel' value? – jwvh Feb 19 '21 at 09:39
  • Sorry, my bad, `Right(myModel)` was just a placeholder. Yes, the idea is is that in the method `createCar` an operation on `myModel` is performed, which returns a new instance of `Model`, i.e., `val newModel: Model = myModel.createCar(record)` and then `Right(newModel)`. I have updated my question. – John Doe Feb 19 '21 at 10:39
-1

You can do that using fold like that:

def processCars(myModel: Model, records: List[Record[Any]]): Either[CarError, Model] = {
  records.foldLeft[Either[CarError, Model]](Right(myModel))((m, r) => {
    m.fold(Left.apply, { model =>
      createCar(model, r).fold(Left.apply, Right.apply)
    })
  })
}
Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35