Skip to content

Latest commit

 

History

History
194 lines (136 loc) · 9.17 KB

2018-10-27-pseudo-dynamically-typed-errors-in-purescript.md

File metadata and controls

194 lines (136 loc) · 9.17 KB

Pseudo-dynamically typed errors in PureScript

While we have many ways to work with possible errors in PureScript like catching them in transformers or representing them as an effect of their own in some library, I have found that in many cases, I have wanted to work with the laziest solution many times: shove it in Error. And while I would not recommend that people who care about correctness when it comes to errors would use this, many could build layers on top of what I will introduce here with a newtype of Effect that tracks the "variety of errors" in an application.

The Problem

Whether you're working in a team of people who are all very familiar with functional programming (i.e. can comfortably work with monad transformers e.g. ExceptT) or you have a team of relatively new PureScript users, you might have some various gripes about working with some kind of ExceptT MyError Aff -- primarily, that you have to now handle two layers of errors: one layer which is from the running of the ExceptT and one that is from Error that are caused by Effect and Aff.

So if you will handle these errors in some "root" context anyway, why not stuff errors that you want to handle into Error and retrieve them later, in operations like Aff.attempt :: forall a. Aff a -> Aff (Either Error a).

But wait, how does one create Error anyway? The only function to create Error normally seems to be Effect.Exception.error :: String -> Error.

A not-so-good "easy" solution

So many people will see String and immediately think "ah ha! I can serialize JSON into that with a concrete type that I will attempt to read out to!" But now you pay excess costs for your errors: you have to try to parse this error JSON string every time into multiple candidates, and you shouldn't really need to encode something just to read it back out if you have a language where you can push more burden of proof into the type system. So how would we do this?

Say hello to everyone's best friend: class subtyping

Okay, you probably don't actually love subtyping, but this is a good case in which we can use a subtype of Error and shove it into Error contexts, and there's a fairly low cost to retrieving our subtype in that we can simply run errorValue instanceof ErrorSubtype (see https://en.wikipedia.org/wiki/Liskov_substitution_principle)

So we can define our subtype and a function to create an error in JS like so:

function VariantError(variant) {
  this.variant = variant;
  this.message = "VariantError";
  return this;
}

VariantError.prototype = Object.create(Error.prototype);
VariantError.prototype.constructor = VariantError;
VariantError.prototype.name = "VariantError";

exports._mkVariantError = function(variant) {
  return new VariantError(variant);
};

Awesome! Now we have a Error subtype with a variant property that we will write to. And as you might have guessed, we will stuff a Variant value into the error, since this lets us be flexible about which "sorts"/variants/members of an error set we can have, so that we don't have to be bound by a (by definition) closed sum type.

For a quick reminder of what Variant enables: consider that sums and products are complements of each other. We normally work with extensible records in PureScript that are polymorphic for some remaining fields, so what if we have a complement of records that can have one of both a defined and extensible set of values? That is Variant.

Our PureScript interface to VariantError

So we can write a PureScript interface to this VariantError that we have defined, by defining a foreign data type and some related functions that follow the substitution principle.

foreign import data VariantError :: # Type -> Type

mkVariantError :: forall r. Variant r -> Error
mkVariantError = upcastVariantError <<< _mkVariantError

upcastVariantError :: forall r. VariantError r -> Error
upcastVariantError = unsafeCoerce

foreign import _mkVariantError :: forall a r. a -> VariantError r

Amazing! And while we might not use this for very much of our regular PureScript code, having the ability to do this in libraries is quite liberating.

How do we extract our Variant?

Now let's consider what all we need to do to extract our variant value out of an Error to be able to use it.

First, we need to make sure that we check if we have a VariantError in our Error. We can check for this as mentioned above by using errorValue instanceof VariantError, so this part is already done for us.

However, then we need to consider how we even verify that the contained Variant value can be worked with. While we could use Simple-JSON to read the contained value into our type, that's a lot of wasted time for reading out an error, especially considering that we will use these errors inside of our application and they are not distributed by libraries, which would introduce a risk of key collisions. If we can be sure that keys will not collide for the Variant values that we work with, we should be able to only check the key. The question is, how should we do this?

Say hello to everyone's actual best friend: RowToList

Turns out, this crazy guy named Justin has been writing all this shit about RowToList, and we can use this to write a type class to reflect the keys of a row type to String. Wild, right?

And so, with classic RowToList code, we can write a class that can iterate the keys of the row type of a specified Variant, so we can check if a given Variant value is actually a value we want to handle:

class MatchKey (r :: # Type) where
  matchKey :: RProxy r -> String -> Boolean

instance matchKeyInst ::
  ( MatchKeyImpl rl
  , RL.RowToList r rl
  ) => MatchKey r where
  matchKey _ = matchKeyImpl (RLProxy :: RLProxy rl)

class MatchKeyImpl (rl :: RL.RowList) where
  matchKeyImpl :: RLProxy rl -> String -> Boolean

instance nilMatchKey :: MatchKeyImpl RL.Nil where
  matchKeyImpl _ _ = false

instance consMatchKey ::
  ( MatchKeyImpl tail
  , IsSymbol name
  ) => MatchKeyImpl (RL.Cons name ty tail) where
  matchKeyImpl _ s = do
    let curr = reflectSymbol (SProxy :: SProxy name)
    if s == curr
       then true
       else matchKeyImpl (RLProxy :: RLProxy tail) s

Then we can write some PureScript functions for reading our Variant values:

readVariant :: forall r. MatchKey r => Error -> Maybe (Variant r)
readVariant err =
  _getVariant <$> FU.runFn4
    _readVariantError
    (matchKey (RProxy :: RProxy r))
    Nothing
    Just
    err

getVariant :: forall r. VariantError r -> Variant r
getVariant = _getVariant

foreign import _readVariantError
  :: forall a b r
   . FU.Fn4
       (String -> Boolean)
       (Maybe b)
       (a -> Maybe a)
       Error
       (Maybe (VariantError r))

And so, we can accordingly write an implementation in JS that uses this predicate and returns the correct values:

exports._readVariantError = function(test, nothing, just, error) {
  if (error instanceof VariantError) {
    var variant = error.variant;
    if (test(variant.type)) {
      return just(error);
    } else {
      return nothing;
    }
  } else {
    return nothing;
  }
};

Nice! Now we have the various pieces we need to work with.

Example Usage

Say have some row type for possible errors from fetching:

otherS = SProxy :: SProxy "other"
readForeignS = SProxy :: SProxy "readForeign"

type Error = V.Variant (ErrorRow ())

type ErrorRow r =
  ( other :: EE.Error
  , readForeign :: F.MultipleErrors
  | r
  )

Then when we work with a function that will insert VariantError into Error in Aff, we can use guards to get the error from the Left constructor when using Aff.attempt:

  -- gives an error in readForeign on mismatched decoding type
  wrong <- Aff.attempt $ fetch' testUrl O.defaultFetchOptions'

  case wrong of
    Left e
      | Just (variant :: O.Error) <- C.readVariant e
      , Just multipleErrors <- V.prj O.readForeignS variant -> do
      pure unit -- success!

    Right (e :: { asdf :: String }) -> Aff.throwError $
      Aff.error "False parsing result"

    Left e -> Aff.throwError e

Cool, right? If we couldn't extract the error with the guarded Left branch above, then we fall over to handling the generic Error, just like we wanted!

Conclusion

While teams of experienced PureScript users may prefer not to use such an approach like this, working with subtypes of Error lets us very easily put together a bunch of Aff and other effect functions together without having to deal with the wiring too much, and lets us easily handle the success, known error, and unknown error cases in the same level.

If one still likes this idea but wants to expand it further, they might consider defining an Eff like newtype for the errors, which can then be eliminated by using row type constraints to remove elements. Such an idea is more thoroughly explored in the Checked-Exceptions library by Nate Faubion.

Otherwise, I hope this gives some insights into other ways in which working with subtypes can be made simple enough in PureScript.

Links