Search code examples
kotlinfunctional-programmingarrow-kt

With Arrow: How do I apply a transformation of type (X)->IO<Y> to data of type Sequence<X> to get IO<Sequence<Y>>?


I am learning functional programming using Arrow.kt, intending to walk a path hierarchy and hash every file (and do some other stuff). Forcing myself to use functional concepts as much as possible.

Assume I have a data class CustomHash(...) already defined in code. It will be referenced below.

First I need to build a sequence of files by walking the path. This is an impure/effectful function, so it should be marked as such with the IO monad:

fun getFiles(rootPath: File): IO<Sequence<File>> = IO {
   rootPath.walk() // This function is of type (File)->Sequence<File>
}

I need to read the file. Again, impure, so this is marked with IO

fun getRelevantFileContent(file: File): IO<Array<Byte>> {
   // Assume some code here to extract only certain data relevant for my hash
}

Then I have a function to compute a hash. If it takes a byte array, then it's totally pure. Making it suspend because it will be slow to execute:

suspend fun computeHash(data: Array<Byte>): CustomHash {
   // code to compute the hash
}

My issue is how to chain this all together in a functional manner.

fun main(rootPath: File) {
   val x = getFiles(rootPath) // IO<Sequence<File>>
      .map { seq -> // seq is of type Sequence<File>
         seq.map { getRelevantFileContent(it) } // This produces Sequence<IO<Hash>>
      }
   }
}

Right now, if I try this, x is of type IO<Sequence<IO<Hash>>>. It is clear to me why this is the case.

Is there some way of turning Sequence<IO<Any>> into IO<Sequence<Any>>? Which I suppose is essentially, probably getting the terms imprecise, taking blocks of code that execute in their own coroutines and running the blocks of code all on the same coroutine instead?

If Sequence weren't there, I know IO<IO<Hash>> could have been IO<Hash> by using a flatMap in there, but Sequence of course doesn't have that flattening of IO capabilities.

Arrow's documentation has a lot of "TODO" sections and jumps very fast into documentation that presumes a lot of intermediate/advanced functional programming knowledge. It hasn't really been helpful for this problem.


Solution

  • First you need to convert the Sequence to SequenceK then you can use the sequence function to do that.

    
    import arrow.fx.*
    import arrow.core.*
    import arrow.fx.extensions.io.applicative.applicative
    
    val sequenceOfIOs: Sequence<IO<Any>> = TODO()
    
    val ioOfSequence: IO<Sequence<Any>> = sequenceOfIOs.k()
        .sequence(IO.applicative())
        .fix()