The example in this post is 100% compatible in PureScript as well, with the exception of the effect monad which is called
Effect in PureScript. Replacing
Effect is all you have to do, as well as use the libraries available there.
I think everyone who's in and around Haskell & PureScript hear some variant of "Every program you make always ends up having a bunch of
IO () in it all the time anyway." and they're not necessarily wrong. The complaint is that you have this neat type system that should constrain your effects but all you get is
Effect, which just plain permits everything.
It's not untrue, but it's not exactly the full story either. Consider the following function:
downloadFile' :: Link -> IO (Response ByteString) downloadFile' (Link link) = Http.get link `Exception.catch` (\(HttpExceptionRequest req (StatusCodeException resp bytestring)) -> do case resp ^. responseStatus . statusCode of 404 -> putStrLn $ "ERROR: File not found (404) for " <> link code -> putStrLn $ "ERROR: Unknown error with code " <> show code <> " for " <> link pure $ fmap (const (LBS.fromStrict bytestring)) resp)
We can of course see that we're trying to download a file. But there's a lot going on that doesn't really have anything to do with downloading files in here. In total, the effects we are dealing with:
- We're using HTTP functions:
- We're dealing with exceptions:
- We're printing to the terminal:
How can we be clearer in our types about what we're doing in a lightweight way?
Well, let's make some constraints:
class MonadTerminalIO m where putStrLnM :: String -> m () instance MonadTerminalIO IO where putStrLnM = putStrLn class MonadHttp m where httpGetM :: String -> m (Response LBS.ByteString) instance MonadHttp IO where httpGetM = Http.get
Exception.catch already has an associated type class/constraint, so we don't need to make that one.
We can now transform our function to the following:
downloadFile :: (MonadHttp m, MonadTerminalIO m, Exception.MonadCatch m) => Link -> m (Response ByteString) downloadFile (Link link) = httpGetM link `Exception.catch` (\(HttpExceptionRequest req (StatusCodeException resp bytestring)) -> do case resp ^. responseStatus . statusCode of 404 -> putStrLnM $ "ERROR: File not found (404) for " <> link code -> putStrLnM $ "ERROR: Unknown error with code " <> show code <> " for " <> link pure $ fmap (const (LBS.fromStrict bytestring)) resp)
We're now being a lot clearer with what we're doing in our type signature and all it took was a couple of type classes and a generic return monad type.
Because we now return
m (Response ByteString) and have a set of constraints on
m instead, we've guaranteed that there can't be any random
IO or other effects in our function, because those are in fact not valid for any
m the type system can imagine. When we try to add something that can talk to the network, for example, it would have to have an associated constraint called
MonadNetwork, for example, and we would have to add it to our constraints to make those functions available in that scope.
If we find ourselves wanting better type signatures, they could be just a few small constraints / type classes away. It's a very effective way to limit the capabilities of a function and be very clear about what's happening inside of it.
This is all possible because of type classes which serve as constraints on generic type variables, as well as higher-kinded types that allow us to talk generically about types wrapping types and together they form something I really like about Haskell & PureScript.