3
votes

I'm kind of new on purescript and I was experimenting with effects and particular async effects.

One of the things I love the most about FP and strict compilers like the one purescript has is that it enforces you to handle all possible results, in particular when you define that something can fail. If you for example are using an Either, you need to tell the program what to do in case you have the Right answer or an error.

When I first looked at effects I liked the concept of actions and handlers and that if a part of your code needs to throw an exception (I imagine this is the last resource you want to use) you need to declare it using something like

someAction :: forall eff. Eff (exception :: EXCEPTION | eff)

and that you can define a handler that removes that effect so you know that from that point onwards you don't have to care about exceptions.

But doing some basic tests with the Aff monad and the purescript-node-fs-aff library I got some unexpected results.

If I do something like this

main :: forall e. Eff (console :: CONSOLE, buffer :: BUFFER, fs :: FS | e) Unit
main = do
  _ <- launchAff $ readAndLog "./non-existing-file" 
  pure unit

readAndLog :: forall eff. String -> Aff (fs :: FS, console :: CONSOLE | eff) Unit
readAndLog path = do
  str <- readTextFile UTF8 path
  log str

If the file doesn't exists the program will terminate throwing an exception and there is nothing telling me that this code can fail, and that I should try to protect my program agaisnt that failure.

I can in fact be a little more defensive and use a catchError, but I was expecting that at least the compiler fail saying I wasn't taking exception as a possible side effect.

main :: forall e. Eff (console :: CONSOLE, buffer :: BUFFER, fs :: FS | e) Unit
main = do
  _ <- launchAff $ readAndLog "./non-existing-file" `catchError` (\e -> log ("buu: " <> message e))
  pure unit

readAndLog :: forall eff. String -> Aff (fs :: FS, console :: CONSOLE | eff) Unit
readAndLog path = do
  str <- readTextFile UTF8 path
  log str

Ideally I would like to do something like Either and be responsible to handle the particular errors the operation may have. For example when I read a file I should expect to have an error like ENOENT (file does not exist) or EACCES (you don't have access), etc. If I want to ignore the particular reason and just log that it failed it's my choice but the type system should enforce me to handle it.

2
In the pursecript channel I was refered to this issue github.com/slamdata/purescript-aff/issues/137 - Hernan Rajchert
The problem really is that launchAff swallows the errors. It should require the caller to provide instructions (i.e. callback) for what to do with errors, but instead it just ignores them. - Fyodor Soikin

2 Answers

1
votes

There are multiple facets to your question.

First, effect rows are soon being removed from standard practice. That has begun with v0.12. Here is a public opinion poll on the matter. The discussions are on GitHub. In a nutshell: the burden outweighs the benefit.

Then there is the problem of knowing which exceptions could be thrown. If you use 3rd party JavaScript anywhere, that will be difficult. In this case I recommend thinking of known exceptions as a lower bound of all exceptions which may be thrown. In other words, even if you catch all known exceptions you must also make a default catch to account for the unknown.

Take a look at Nathan Faubion's purescript-checked-exceptions library. This shows how to apply checked exceptions to an ExceptT interface.

I have similar work done for JavaScript's native exceptions which is appropriate for FFI. Unfortunately that is not published yet but I will hopefully be able to do so soon. At least you can know it is possible.

The final facet is Aff itself. Unfortunately, Aff is not designed for arbitrary exceptions. It only supports the Error type (i.e. the JavaScript type of the same name). Therefore, you would be better to use purescript-checked-exceptions with Aff to add checked exceptions.

0
votes

The compiler would not tell you that because exceptions are assimilated by the Aff machinery in the library you are using. Here are the relevant pieces:

readTextFile = toAff2 A.readTextFile
toAff2 f a b = toAff (f a b)
toAff p      = makeAff \e a -> p $ either e a

Where A.readTextFile is the async version in the Node library.

The expression either e a is where it happens. As Aff has a MonadError, your only recourse is indeed to catch the error. That said, I completely agree with you that the effect should expose the fact exceptions may be thrown. For some reason, only the synchronous version of the Node library does.