Search code examples
scalascalazio-monad

Composing scalaz io effect objects


I'm trying to use scalaz's ioeffect IO[E,A] monad for a piece of code that is very effectful.

The code I'm trying to rewrite with IO[E,A] at a high level takes some metadata about a file that's store in a cloud. The code attempts to:

  1. download the file
  2. extract strings from the file
  3. build a pojo that contains the file text contents
  4. submits the pojo to some queue/restful service

The details of the steps aren't that important, but what I was thinking of doing was to do something along the lines of:

def processShareActivity(fileObject: FileObject): IO[ProcessFileFailure, IndexResponse] = {
    for {
        file <- downloadFile (fileObject)
        text <- extractText (file)
        searchFileObject <- IO.point(buildSearchFileObject (fileObject, file, text))
        indexedResponse <- indexSearchFileObject (searchFileObject)
    } yield indexedResponse
}

def indexSearchFileObject(fileObject: SearchFileObject): IO[IndexFailure, IndexResponse] = ???

def buildSearchFileObject(fileObject: FileObject, file: File, str: String): SearchFileObject = ???

def extractText(file: File): IO[ExtractionFailure, String] = ???

def downloadFile(fileObject: FileObject): IO[DownloadFileFailure, File] = ???

The problem is that an instance of IO[E,A] and IO[F,B] do not seem to compose. That is, since for example downloadFile's IO signature returns DownloadFileFailure for its error scenario and extractText returns ExtractionFailure, those monads can't seem to compose in the for comprehension.

Is there an easy way for my top level for comprehension to compose such that it results in a IO[ProcessFileFailure, IndexResponse] where ProcessFileFailure is some kind of a wrapping failure object around the different kind of failures that can happen in the submethods?


Solution

  • Unfortunately, you do need a way to unify these errors into a common one:

    for example:

    sealed trait ProcessFileFailure
    object ProcessFileFailure {
       case class Index(e: IndexFailure) extends ProcessFileFailure
       case class Extraction(e: ExtractionFailure) extends ProcessFileFailure
       case class Download(e: DownloadFileFailure) extends ProcessFileFailure
    }
    

    And your for-comprehension will become:

    for {
            file <- downloadFile (fileObject).leftMap(ProcessFileFailure.Download)
            text <- extractText (file).leftMap(ProcessFileFailure.Extraction)
            searchFileObject <- IO.point(buildSearchFileObject (fileObject, file, text))
            indexedResponse <- indexSearchFileObject (searchFileObject).leftMap(ProcessFileFailure.Index)
        } yield indexedResponse
    

    Its awkward, but it does have the advantage of being able to store everything that went wrong and exactly the context in which it happened.