Flattening Nested Eithers in PureScript

Let’s say I have a function which reads a sftp-config file into a String.

readSftp :: forall e
   . FilePath
  -> Eff (fs :: FS | e ) (Either Error String)
readSftp filePath = do
  let configPath = filePath <> "/sftp-config.json"
  fileText <- try (readTextFile UTF8 configPath)
  pure $ rmap minify fileText

If I want this function to automatically decode the String into some data type, I would end up with a nested Either in my function signature. This is not particularly nice to work with.

readSftp :: forall e
   . FilePath
  -> Eff (fs :: FS | e ) (Either Error (Either Error Sftp))
readSftp filePath = do
  let configPath = filePath <> "/sftp-config.json"
  fileText <- try (readTextFile UTF8 configPath)
  pure $ rmap (minify >>> decodeSftp) fileText

decodeSftp :: String -> Either Error Sftp
decodeSftp s =
  lmap flattenMultipleErrors $
    runExcept $ genericDecodeJSON
      sftpGenericOpts s

Wouldn’t it be nice if we could flatten the nested Eithers and go back to Either Error Sftp? Well we can. Using the either function, we can map the outermost Left back into Left and the outermost Right back into itself using id.

readSftp :: forall e
   . FilePath
  -> Eff (fs :: FS | e ) (Either Error Sftp)
readSftp filePath = either Left id <$> do
  let configPath = filePath <> "/sftp-config.json"
   fileText <- try (readTextFile UTF8 configPath)
   pure $ rmap (minify >>> decodeSftp) fileText

This made me wonder if it’s possible to flatten an arbitrarily deep list of Eithers. I tried to write an function to do that, but I don’t think it’s possible. Take a look at the two implementation attempts below…

-- This doesn't work because, we can't
-- ever have a success case, since the success case
-- is always the next link in the chain. This is
-- essentialy `List` without the `Nil` constructor
type EitherStream e a = Either e (EitherStream e a)

flattenEither :: forall e a. EitherStream e a
flattenEither (Left  e) = e
flattenEither (Right e) =
  -- e will always be the next link in the chain,
  -- so we can only end the computation on the failure case
  -- and never on the success case.
-- If we define `flattenEither` using only either,
-- we can't recurse into the structure
flattenEither :: forall e a. EitherStream e a
flattenEither (Left  e) = e
flattenEither (Right e) =
  -- We have no guarantee that e is an `Either e (Either e a)`
  -- so we can't recursively process it.