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
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 ). Not sure if I prefer it or not.