Kotlin-esque way to iterate w/ a different first step


#1

I get the feeling that this is a horrible way to do things, and I’m sure there is a Kotlin beautiful way to do it.

When you need to iterate through something, but you need to do some setup the first time, is there a better structure that could handle it? (like a fold or a nicer use of elvis)

Example 1: a frame grabber that needs a call to “start()” This works fine, but I’d much prefer if the grabber.start() and grabber.close() (and the grabber in general) weren’t left floating, and instead were tucked into the generateSequence block.

val grabber = FFmpegFrameGrabber("input.mp4")
grabber.start()
val frames = generateSequence { grabber.grabImage() }
frames.forEach { ... }
grabber.close()

Example 2: Walk a path, but the first step needs to be from the document root instead of the current element. (not a problem in DOM because it IS an element, but is a problem with firestore). All the lateinit and index==0 feels… smelly.

private val docRef: DocumentReference = {
    require(path.isNotEmpty()) { "Empty path: Can't insert documents into a firestore root."}
    lateinit var tmpDocRef: DocumentReference
    path.forEachIndexed { it, (collectionId, documentId)->
        tmpDocRef = if(it==0) {
            db.collection(collectionId).document(documentId)
        } else {
            tmpDocRef.collection(collectionId).document(documentId)
        }
    }
    logger.info { "Attached to ${tmpDocRef.path}" }
    tmpDocRef
}()

#2

I don’t have my laptop to test it, but I’m pretty sure you can write the first one as:

val frames = buildSequence {
    val grabber = FFmpegFrameGrabber("input.mp4")
    grabber.start()
    yield(grabber.grabImage())
    grabber.close()
}
frames.forEach { ... }

#3

Note that this way you need to iterate the sequence to the end, otherwise grabber would be left open. One has to be careful not to call some early terminating operation on that sequence, such as take(n), any { ... } / all { ... } etc.


#4

Nice! One down, one to go…

val frames = sequence<Frame> {
    val grabber = FFmpegFrameGrabber("input.mp4")
    grabber.start()
    while(true) {
        yield(grabber.grabImage()?:break)
    }
    grabber.close()
}

lateinit var ffr: FFmpegFrameRecorder
frames.forEachIndexed { idx, frame ->
    println(idx)
    if (idx == 0) {
        ffr = FFmpegFrameRecorder("out.mp4", frame.imageWidth, frame.imageHeight, 0)
        ffr.frameRate = 60.0
        ffr.videoBitrate = 0 // max
        ffr.videoQuality = 0.0 // max?
        ffr.start()
    }
    ffr.record(frame)
}
ffr.stop()

#5

Please note the issue cited by @ilya.gorbunov, you can replace the iterator pattern with the visitor pattern, so consider to define the function:

fun FFmpegFrameRecorder.forEachFrameIndexed(...)

The example 2 looks like a fold, using initial = db.collection(collectionId).document(documentId) and operation = tmpDocRef.collection(collectionId).document(documentId)

Moreover consider to replace the { ... }() syntax with a bit more explicit run { ...}


#6

Things like close() should be called in finally { ... } clause for extra safety. So IMO, it’s not good to put entire iteration calls inside some kind of sequence, because sequence is not guaranteed to finish.


#7

Er… like wrap the whole thing in a try/finally?


#8

I’m not entirely sure what you mean by replacing with a visitor pattern here.


#9

I dump some code here

fun FFmpegFrameGrabber.forEachFrameIndexed(step: (Pair<Int,Frame>) -> Unit) {
 start()
 try{
   var i = 0
   var frame = grabber.grabImage()
   while (frame != null) {
     step(i to frame)
     frame = grabber.grabImage()
     i++
   }
 } finally {
   close()
 }
}

#10

The way I would do this is to have an extension function FFmpegFrameGrabber.forEachImage that basically encapsulates all the access logic (including the closing of it, which should be ensured with use or try finally).

For your second problem I would go to the idea of creating extension functions like first and rest that allow you to explicitly access things. Depending on the actual underlying source they can be syntactic sugar if the act of reading the first element consumes it (so only the rest is there)


#11

#1: I got lucky because it is closeable, so I can use use. (which should close even if not fully consumed)

val frames = sequence<Frame> {
    FFmpegFrameGrabber("input.mp4").use { grabber ->
        grabber.start()
        println("Total frames: ${grabber.lengthInVideoFrames}")
        while (true) {
            yield(grabber.grabImage() ?: break)
        }
    }
}

#12

@fvasco that worked brilliantly, thank you!

private val docRef: DocumentReference = path.drop(1).fold(db.collection(path.first().first).document(path.first().second)) { agg, nextPath ->
    agg.collection(nextPath.first).document(nextPath.second)
}.also { logger.info { "Attached bot to ${it.path}" } }

#13

No, it shouldn’t. There’s no way the sequence coroutine could be resumed after yield if the sequence is not iterated entirely.


#14

Bummer. But, still pretty happy with this comment solution!


#15
for (frame in frames) error("sequence not fully consumed")