Is there an encapsulation of 'Effect+Either'?

Hello everyone, I am learning purescript.

I have encapsulated some functions, such as a function to read a file, its signature may be like this:

myReadFile :: String -> Effect String

But this may go wrong, Although there is a try function to catch any errors that occur, and get a Either.

But I want to return different error prompts under different circumstances, so I changed it to this:

myReadFile :: String -> Effect (Either String String)

It looks good now, when there is an error reading the file, I will get a Left String, This is an error message, And when there is no error I will get a Right String, This is the content of the file I need.

The only problem is that this type has a lot of inconvenience to deal with, For example, if I have a function:

myFun :: String -> String -> String

Now, I need to input the content of the file and the string “abc” into this function, In a do region, I would write like this:

do
  e_str <- myReadFile "./aaa.txt"
  e_s <- lift2 myFun e_str (pure "abc")
  ...

In complex situations, I would also use sequence join and other functions to adjust the type.

Why not encapsulate “Effect” and “Either” into a new type? After all, most operations with side effects may go wrong.

E.g:

data EEffect a = EEffect (Effect (Either String a))

toNatural :: forall a. EEffect a -> Effect (Either String a)
toNatural (EEffect a) = a

instance functorEEffect :: Functor EEffect where
  map :: forall a b. (a -> b) -> EEffect a -> EEffect b
  map f v =
    EEffect
      $ do
          vv <- toNatural v
          pure $ map f vv

my question:

  1. Is this idea correct?
  2. If feasible, is there a similar type, but I did not see it?
3 Likes

Welcome @lsby!

There is an existing similar type - Effect! And your toNatural function is just try.
Cheekiness aside, I think you’re butting up against some of the ideas in this blog post (forgive that it’s Haskell instead of PureScript, but most of the ideas translate pretty well).
If you’re trying to capture all errors in an Either, and pretend that Effects can’t crash on you, you’ll end up fighting the ecosystem and end up with some nasty surprises when your Effects do crash. The truth is all Effects can throw, and by introducing an Effect (Either _ _), you’re just adding a second “channel” that errors can propagate through, and you’ll have to remember to deal with both channels.

Now one case where you’ll frequently see Effect (Either e a) or Aff (Either e a) is when the e is some other type, like an ADT. When Effect or Aff errors out, it’s just an Error, which can’t really carry any information beyond the String message. So if you need to handle different errors in different ways, you’ll need to break away from the builtin Error type. But for String errors, just throw them in the Effect. You’ve got to have an error handler anyway for the cases where your Effect does crash.

You might be interested in going the other direction, and looking for a “sanitized” Effect - one that’s guaranteed not to throw any errors (because they’ve all been handled). I’m sure that at one point I ran across a library that provided this, but I can’t find it for the life of me. That would address your concerns, but you couldn’t make Effect sanitized, because Effect is used for native JavaScript effects, which can throw.

Some other related reading for the Aff monad where some discussion has gone on about adding an explicit error channel instead of an implicit one that only takes type Error:

(@hdgarrood cites that same article about error handling there).

2 Likes

The type that encapsulates this is ExceptT from transformers.

newtype ExceptT e m a = ExceptT (m (Either e a))
type EEffect = ExceptT String Effect

Note, if you have a pure function, you don’t need to bind anything. You can use let inside do-blocks.

example = do
  e_str <- myReadFile "./aaa.txt"
  let e_s = myFun e_str "abc"
  ...

I wouldn’t really use this blog post as an example of a “best practice” in PureScript. We basically never use host-runtime exceptions in the PureScript ecosystem. Haskell has a whole consumable error hierarchy, but PureScript doesn’t, so there’s almost no utility in catching Error except for cleanup. I think it’s maybe fine to use exceptions if you don’t expect users to recover from particular cases, but otherwise we definitely use Either out of necessity!

3 Likes

I should have mentioned ExceptT in my initial post, which eases a lot of the rough edges around an Effect (Either e a). I recommend working through chapter 11 of the PureScript book if you want to learn more about how they work. IMO, you should avoid specifically ExceptT String Effect a, since if you’ve arrived at the point where all you can do for an error is assemble a String message, then you’re probably at the cleanup phase where an exception will suit you better. You run the risk of implying that all error messages can only come through the Either, when there are plenty of examples where you can hit exceptions too, and you have to remember to handle both (such as the readTextFile function, which will crash if the file isn’t accessible).

Please though use ExceptT for cases where more error information is available/useful besides just assembling a String error message.

I agree, I’d also use ExceptT here. PureScript is in no way the same as Haskell is as far as exceptions go. Actually, the only circumstances when I ever had runtime errors were times when foreign modules produced them (but I only ever developed for browser). I perceive the Effect monad as a much safer object than Haskell’s IO. The dragons live in IO.

Thanks for everyone’s reply

I didn’t realize this before.

Any Effect can use throwException to throw an exception, and use the try function to catch it and get Either.

For simple cases, I think this is enough.So there is no need to add the (Either _ _) channel.

Yes, I hope that my program will not produce any runtime errors. I hope that the type system can indicate all possible errors. I only need to pay attention to the handling of JavaScript code errors.

After referring to that blog, I realized that ExceptT is another method. Compared with throwException, ExceptT can provide more information and help the program recover from errors. But also like that blog post That said, there seem to be some unsatisfactory aspects.

As far as I am concerned, I think it is unreasonable to have two “channels” at the same time.

Because ExceptT can also throw an exception through throwException, causing runtime errors:

module Main where
import Prelude
import Control.Monad.Cont (lift)
import Control.Monad.Except (ExceptT, runExceptT)
import Data.Either (Either(..))
import Effect (Effect)
import Effect.Console (log)
import Effect.Exception (error, throwException)

myReadFile :: ExceptT String Effect String
myReadFile = do
  _ <- lift $ throwException $ error "err"
  lift $ pure "file text"

main :: Effect Unit
main = do
  e_s <- runExceptT myReadFile
  case e_s of
    Left err -> log err
    Right a -> log a

Therefore, for the value of ExceptT String Effect String, both Either and throwException thrown by throwError must be handled. I think this is not safe enough.

For me, it is enough to throw a string after an error, prompt the user, and then stop the program.
Compared to the more information provided by Either, I prefer the code to be as robust as possible.

So I would rather write it like this: (I just need to add a try to main)

module Main where

import Prelude
import Data.Either (Either(..))
import Effect (Effect)
import Effect.Console (log)
import Effect.Exception (error, message, throwException, try)

myReadFile :: Effect String
myReadFile = do
  _ <- throwException $ error "err"
  pure "file text"

main :: Effect Unit
main = do
  e <-
    try do
      e_s <- myReadFile
      log e_s
  case e of
    Left err -> log $ message err
    Right _ -> pure unit

Thank you all, I have learned a lot.

1 Like