Custom errors with instance chains

I’m trying to work out how to add custom error messages. I would have thought that I can create a type class with one instance for the success and a second instance in chain to provide an error message. Here is a small example that I was hoping would work:

import Prim.Row (class Lacks)
import Prim.TypeError (class Fail, Text)
import Type.Proxy (Proxy(..))

class MustLack :: forall k. Symbol -> Row k -> Constraint
class MustLack key row

instance doesLack :: ( Lacks key row ) => MustLack key row
else instance doesNotLack :: Fail (Text "Already has key in row") => MustLack key row

check ::
  forall key value row.
  MustLack key row =>
  Proxy key -> value -> Record row -> Boolean
check _ _ _ = true

_foo_ = Proxy :: Proxy "foo"

r = check _foo_ 5 { foo: "Hello" }

However the error message is just about how the first instance’s constraint fails:

  No type class instance was found for
                                    
      Prim.Row.Lacks "foo"          
                     ( foo :: String
                     )              
                                    

  while solving type class constraint
                                     
    MustLack.MustLack "foo"          
                      ( foo :: String
                      )      

I’d guess this must mean that discriminant between instances can’t be in the constriant but in the type? So how would you go about adding a custom error message in this case?

I think some more documentation would be appreciated for custom error messages as the only example I can find is for warning messages on deprecated functions. It’d be good to see examples of when it’s useful to use Fail.

Thanks for any and all help.

What you want is “backtracking” in the instance chain. Go to this page on my learning repo and scroll down to the ## Instance Chains Gotchas: No Backtracking section. That will explain why this doesn’t work.

Thanks Jordan! That explanation makes sense :blush:

This thread seems related Allow guards in instance chains · Issue #3120 · purescript/purescript · GitHub and someone has already mentioned use for custom type errors.

For posterity, the example can be solved by using a RowList like so:

import Prim.RowList (class RowToList, Cons, Nil, RowList)
import Prim.TypeError (class Fail, Text)
import Type.Proxy (Proxy(..))

class MustLackRow :: forall k. Symbol -> Row k -> Constraint
class MustLackRow key row

instance mustLack :: (RowToList row rowList, MustLackRowList key rowList) => MustLackRow key row

class MustLackRowList :: forall k. Symbol -> RowList k -> Constraint
class MustLackRowList key rowList

instance mustLackRowList_Fail :: Fail (Text "Already has key in row") => MustLackRowList key (Cons key value tail)
else instance mustLackRowList_Looking :: ( MustLackRowList key tail ) => MustLackRowList key (Cons otherKey otherValue tail)
else instance mustLackRowList_Nil :: MustLackRowList key Nil

check ::
  forall key value row.
  MustLackRow key row =>
  Proxy key -> value -> Record row -> Boolean
check _ _ _ = true

_foo_ = Proxy :: Proxy "foo"

r = check _foo_ 5 { foo: "Hello" }
1 Like