As a quick note to self, I wrote this Scala code as a way to (a) find the first element in a sequence, and then (b) return that element without traversing the rest of the sequence.
I initially thought about writing this code with a while
loop, for
loop, or for
expression — because I knew I needed a loop and a way to break out of a loop — but then I realized that an iterator would help me out here.
Also, please note that there is a potentially-better solution after this — the one that uses the “view.”
A first solution
So without any further ado, here’s this solution:
/*
* Benefits of the iterator/map/find/flatten approach:
* 1. It stops processing as soon as it finds a match (efficient).
* 2. If it finds a match, it returns that match, wrapped in another Some.
* 3. If it doesn't find any match, it returns None.
*/
def loopThruSmallImagesToFindOneThatWorks(
lgImage: BufferedImage,
lgImageRows: Int,
lgImageCols: Int,
fiveYearIconFilenames: Seq[String]
): Option[Point] =
// create a lazy iterator to process files one at a time.
fiveYearIconFilenames.iterator
.map(filename => tryToFindSmallImageInScreenshot(lgImage, lgImageRows, lgImageCols, filename))
.find(opt => opt.isDefined) // find the first Option that is defined; return it in a Some
.flatten // then you need to flatten that outside Some
Functional programming with iterators
This type of programming is known as “functional programming with iterators.” Other names for it are “stream processing” and “lazy evaluation.” The way the code exits when the first element is found is known as “short-circuiting” or “early termination.”
The iterator approach employs lazy evaluation, which means that it only processes one element at a time, as needed. This can be very efficient when working with large collections, because you don’t have to load the entire collection into memory, but in this case the reason I use it is because I want to exit this code as soon as the first match is found.
Another way to write this code is like this, using map
and collectFirst
:
def loopThruSmallImagesToFindOneThatWorks(
lgImage: BufferedImage,
lgImageRows: Int,
lgImageCols: Int,
fiveYearIconFilenames: Seq[String]
): Option[Point] =
fiveYearIconFilenames.iterator
.map(filename => tryToFindSmallImageInScreenshot(lgImage, lgImageRows, lgImageCols, filename))
.collectFirst { case Some(point) => point }
The potential benefits are:
collectFirst
: This method is a bit more idiomatic and concise. It “collects” the first element that matches the specified pattern, which in this case is the firstSome(point)
.- Elimination of
flatten
: UsingcollectFirst
eliminates the need to callflatten
since it directly extracts thePoint
from theSome
.
Another potential solution: view, flatMap, and headOption
FWIW, I just threw that function into a couple of A.I. tools, and a new suggestion I got for the function code looks like this:
def loopThruSmallImagesToFindOneThatWorks(
lgImage: BufferedImage,
lgImageRows: Int,
lgImageCols: Int,
fiveYearIconFilenames: Seq[String]
): Option[Point] =
fiveYearIconFilenames
.view // Creates a non-strict view of the sequence
.flatMap(filename => tryToFindSmallImageInScreenshot(lgImage, lgImageRows, lgImageCols, filename))
.headOption // Returns the first Some, or None if all are None
Potential benefits of this approach are:
- It uses
view
instead ofiterator
. This creates a non-strict view of the sequence, which is lazy like an iterator, but can be more flexible. - It uses
flatMap
instead ofmap
followed byflatten
. This combines the mapping and flattening operations into one step. (Experienced Scala programmers know this, so this might be an approach they’d use.) - It uses
headOption
instead offind(opt => opt.isDefined).flatten
. This gives you the firstSome
value, orNone
if all results areNone
.
Again, I just want to show another option. As a programmer, I think it’s good to see and know the available options, and then you can decide which solution you like best.