Questions about combining types in Purescript

Question 1:
Is there some smart way to create the following function Either a b -> Either c d -> Either e d. I have a basic implementation that uses nested pattern matching but im trying to use and understand the higher levels of abstractions in Purescript. My attempt to use Apply or Bind has left me with the understanding that Left need to be of the same type for this to work. Is this correct or am i missing something?

Question 2:
Is there a function that does what combineRows should do here?

type RowA r = 
  (a :: Int | r)
type RowB r = 
  (b :: Int| r)
newtype Rows = Rows { | RowA (RowB ())}

combineRows :: { | RowA () } -> { | RowB () } -> Rows
combineRows a b = ???

I have tried using merge from records but it does not make the type checker happy.

Either c isn’t a full type. Did you mean this?

Either a b -> Either c d -> e -> Either e d

And yes, Either e a is a fully concrete type. Either sameErrorType is a monad. And Either is a bifunctor. So, using Either e's bind forces the error type to be the same type in the do notation for that monad.

Is there a function that does what combineRows should do here?

You probably want merge or union

combineRows :: { | RowA () } -> { | RowB () } -> Rows
combineRows a b = Rows $ Record.merge a b

Sorry about the typo, please check OP for the correct type.

So I could use lmap from Bifunctor then to unify Left for both Either’s and then use Bind, if so would that be ideomatic or how is this type of situation handled in Purescript code?

Could you share that? I’m having trouble knowing what a function of type Either a b -> Either c d -> Either e d should even do. e.g., if the second argument is a Left (_ :: c), what gets output? You have neither an e nor a d to return.

What I want is a nice way to do the following,
first use the sync version in node-fs to read a json
try $ readTextFile ASCII "config.json"
then convert the JSON to a type using Foreign like so
runExcept $ decodeJSON s, what I have now works but is probably overly complicated:

main :: Effect Unit
main = do
  conf <- (\e -> lmap  message e) <$> (try $ readTextFile ASCII "config.json")
  decoded <- pure $ conf >>= \s -> lmap show (runExcept $ decodeJSON s)
  requestAVar <- AVar.empty
  case decoded of
        Right fileData -> startApp fileData requestAVar
        Left e -> log e
1 Like

Hmmm, it looks like you’re trying to sequence error-throwing actions in the same do block. This is the usual situation where you may want to do everything in ExceptT instead.

i.e. If you think that instead of this following example (…of reading a file, parsing to JSON, then decoding that json to a domain type) where all of the staircasing happens in Effect (and where we case match on each potential error on the spot)…

main :: -> Effect Unit
main = do
  eResultString <- try (readTextFile UTF8 "IDONTEXIST.json")
  case eResultString of
    Left error ->
      Console.log $ "Couldn't open IDONTEXIST.json. Error was: " <> show error
    Right resultString -> do
      case Argo.jsonParser resultString of
        Left stringError -> Console.error $ "Error parsing string to JSON: " <> stringError
        Right json -> do
          let codec = CAR.object "Person" { name: CA.string, age: CA.number }
          case CA.decode codec json of
            Left jsonErr -> Console.error $ CA.printJsonDecodeError jsonErr
            Right person -> do
              Console.log "'IDONTEXIST.json' has been read/parsed/decoded successfully to the following Person: \n"
              logString $ show person

…that you might prefer the following ‘ExceptT’-based style instead that…

-- | ... handles the error case pattern matching explicitly just once here:
main :: Effect Unit
main = 
  runExceptT exceptExample >>= case _ of
    Left myError ->
      Console.error $ renderMyError myError
    Right person -> do
      Console.log $ "Successfully decoded a person from IDONTEXIST.json: \n"
      Console.logShow person

-- | And that gets a nice cleaner 'do'-block for the 3 actions being
-- | sequenced than the staircasing example:
-- | (i.e. where it is more imperative-like, in that you 1) read in a string, 2)
-- | parse the string to JSON, then 3) decode the JSON to a domain type:

exceptExample :: ExceptT MyError Effect Person
exceptExample = do
  resultString <- liftEitherWith MkFileError $ try (readTextFile UTF8 "IDONTEXIST.json")
  resultJson <- hoistEitherWith MkParseError $ Argo.jsonParser resultString
  resultPerson <- hoistEitherWith MkDecodeError $ CA.decode personCodec resultJson
  pure resultPerson

…then have I got the Gist for you :sweat_smile:
https://try.purescript.org/?gist=06ac4bd0908fc7a748884952efcddece

edit: Cleaned things up a bit as the comments were originally all over the shop and didn’t match what the example had evolved into. I’ve done some further evolving too, but hopefully everything aligns properly now.

3 Likes

Also @favetelinguis, if you just wanted to tidy up your below main block in a way that doesn’t need to scale beyond what’s there when you’re loading your config…

…then you could use some more combinators for the Except type such as except and withExcept (in addition to your runExcept that’s already there).

Like this perhaps:

import Control.Monad.Except (except, runExcept, withExcept)

main :: Effect Unit
main = do
  eitherString <- try $ readTextFile ASCII "config.json"
  let exceptString = withExcept message (except eitherString)
  case runExcept (exceptString >>= decodeJSON >>> withExcept show) of
    Left err ->
      Console.log err
    Right fileData ->
      AVar.empty >>= startApp fileData

startApp :: FileData -> AVar.AVar String -> Effect Unit
startApp config avar = ...

This yak shave is a little less verbose on my part (…this time :sweat_smile: :upside_down_face: ). Not sure if I prefer it or not.