Search code examples
twitter-bootstraphaskellyesod

Haskell Equality Constraint


I'm trying to build bootstrap 3 compatibility for Yesod. However, doing so by making a "renderBootstrap3" function is impossible because you can't add class to inputs. So, I've opted for making bootstrap versions of the fields in Form.Fields. The idea is that I can clone the normal fields but add a class declaration in the attributes array. Here is the relevant code:

import qualified Yesod.Form.Fields as F

injectClass :: (Text -> Text -> [(Text,Text)] -> Either Text a -> Bool -> WidgetT (HandlerSite m) IO ()
           Text -> Text -> [(Text,Text)] -> Either Text a -> Bool -> WidgetT (HandlerSite m) IO ()
injectClass f a b attrs d e = f a b attrs d e

textField :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m Text
textField = addInputClass F.textField

addInputClass :: (Monad m, RenderMessage (HandlerSite m) FormMessage) => Field m a -> Field m a
addInputClass f = f { fieldView = (injectClass $ fieldView f)}

So, my intention is to take the normal version of text field and use record syntax to modify only the fieldView method. This method should be replaced by a one that is identical except for the addition of a class attribute. This is not yet implemented in the code above. It would probably look something like:

injectClass f a b attrs d e = f a b (("class", "form-control") : attrs) d e

Anyway, the problem is that the original code won't compile. I get an equality constraint error:

Could not deduce (HandlerSite m0 ~ HandlerSite m)
from the context (Monad m,
                  RenderMessage (HandlerSite m) FormMessage)
  bound by the type signature for
             addInputClass :: (Monad m,
                               RenderMessage (HandlerSite m) FormMessage) =>
                              Field m a -> Field m a
  at Field/Bootstrap.hs:27:18-95
NB: `HandlerSite' is a type function, and may not be injective
The type variable `m0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
Expected type: FieldViewFunc m a
  Actual type: Text
               -> Text
               -> [(Text, Text)]
               -> Either Text a
               -> Bool
               -> WidgetT (HandlerSite m0) IO ()
In the `fieldView' field of a record
In the expression: f {fieldView = (injectClass $ fieldView f)}
In an equation for `addInputClass':
    addInputClass f = f {fieldView = (injectClass $ fieldView f)}

Notice that FieldViewFunc m a is defined as

type FieldViewFunc m a
    = Text -- ^ ID
   -> Text -- ^ Name
   -> [(Text, Text)] -- ^ Attributes
   -> Either Text a -- ^ Either (invalid text) or (legitimate result)
   -> Bool -- ^ Required?
   -> WidgetT (HandlerSite m) IO ()

So, I'm not far off. The problem is (I think) that it isn't recoginizing that injectClass doesn't change the monad. However, this should be obvious to the compiler. The type signature for injectClass is clear about this. I'm looking for what I need to do to satisfy GHC. Thanks for any help, and let me know if I can be more clear.


Solution

  • Ah, type families and non-injectivity.

    Here's what's going on: we are trying to type-check

    f { fieldView = (injectClass $ fieldView f)}
    

    The problem is not that injectClass might change what m is. The problem is that it doesn't know what m is. Its input, fieldView f and context, setting the fieldView field of f, both only tell it what HandlerSite m is, and the simple fact is you can't work out what m is from there. It's like if you had the following:

    type family F a
    type instance F Int = Bool
    type instance F Char = Bool
    

    Now trying to pass an F Int where an F Char is expected will succeed, because they're both just Bool. Hence, trying to pass a HandlerSite m0 where a HandlerSite m is expected might succeed, even if m is not m0! Hence, the type checker can't be sure that m0 is supposed to be the same as m, so you get an ambiguous type error.

    Moreover, you can't easily resolve the ambiguity with manual annotations, since you'll run into the same problem: you'll be telling the type checker what HandlerSite m should be, which it already knows.

    Can you remove HandlerSite m from the type signature of injectClass entirely, replacing it with just an ordinary type variable h, say? That ought to solve your problem, but I'm not sure if it will create any new ones.