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.