Question

I'm writing a new authentication system for the Snap web framework, because the built-in one isn't modular enough, and it has some features that are redundant/"dead weight" for my application. This problem isn't related to Snap at all, though.

While doing so, I hit a problem with ambiguous type constraints. In the following code, it seems obvious to me that the type of back can only be the type variable b in the functions type, yet GHC complains that the type is ambiguous.

How can I change the following code such that the type of back is b, without using e.g. ScopedTypeVariables (because the problem is with the constraint, not with having too general types)? Is there a functional dependency that is needed somewhere?

Relevant type classes:

data AuthSnaplet b u =
  AuthSnaplet
  { _backend    :: b
  , _activeUser :: Maybe u
  }
-- data-lens-template:Data.Lens.Template.makeLens
-- data-lens:Data.Lens.Common.Lens
-- generates: backend :: Lens (AuthSnaplet b u) b
makeLens ''AuthSnaplet

-- Some encrypted password
newtype Password =
  Password
  { passwordData :: ByteString
  }

-- data-default:Data.Default.Default
class Default u => AuthUser u where
  userLogin :: Lens u Text
  userPassword :: Lens u Password

class AuthUser u => AuthBackend b u where
  save :: MonadIO m => b -> u -> m u
  lookupByLogin :: MonadIO m => b -> Text -> m (Maybe u)
  destroy :: MonadIO m => b -> u -> m ()

-- snap:Snap.Snaplet.Snaplet
class AuthBackend b u => HasAuth s b u where
  authSnaplet :: Lens s (Snaplet (AuthSnaplet b u))

The code that fails:

-- snap:Snap.Snaplet.with :: Lens v (Snaplet v') -> m b v' a -> m b v a
-- data-lens-fd:Data.Lens.access :: MonadState a m => Lens a b -> m b
loginUser :: HasAuth s b u
          => Text -> Text -> Handler a s (Either AuthFailure u)
loginUser uname passwd = with authSnaplet $ do
  back <- access backend
  maybeUser <- lookupByLogin back uname -- !!! type of back is ambiguous !!!
  -- ... For simplicity's sake, let's say the function ends like this:
  return . Right . fromJust $ maybeUser

Full error:

src/Snap/Snaplet/Authentication.hs:105:31:
    Ambiguous type variables `b0', `u0' in the constraint:
      (HasAuth s b0 u0) arising from a use of `authSnaplet'
    Probable fix: add a type signature that fixes these type variable(s)
    In the first argument of `with', namely `authSnaplet'
    In the expression: with authSnaplet
    In the expression:
        with authSnaplet
      $ do { back <- access backend;
             maybeUser <- lookupByLogin back uname;
               ... }

src/Snap/Snaplet/Authentication.hs:107:16:
    Ambiguous type variable `b0' in the constraint:
      (AuthBackend b0 u) arising from a use of `lookupByLogin'
    Probable fix: add a type signature that fixes these type variable(s)
    In a stmt of a 'do' expression:
        maybeUser <- lookupByLogin back uname
    In the second argument of `($)', namely
      `do { back <- access backend;
            maybeUser <- lookupByLogin back uname;
              ... }'
    In the expression:
        with authSnaplet
      $ do { back <- access backend;
             maybeUser <- lookupByLogin back uname;
               ... }
Was it helpful?

Solution

I would venture to guess that the root of your problem is in the expression with authSnaplet. Here's why:

∀x. x ⊢ :t with authSnaplet 
with authSnaplet
  :: AuthUser u => m b (AuthSnaplet b1 u) a -> m b v a

Don't mind the context, I filled in some bogus instances just to load stuff in GHCi. Note the type variables here--lots of ambiguity, and at least two that I expect you intend to be the same type. The easiest way to handle this is probably to create a small, auxiliary function with a type signature that narrows things down a bit, e.g.:

withAuthSnaplet :: (AuthUser u)
                => Handler a (AuthSnaplet b u) (Either AuthFailure u) 
                -> Handler a s (Either AuthFailure u)
withAuthSnaplet = with authSnaplet

Again, pardon the nonsense, I don't actually have Snap installed at the moment, which makes things awkward. Introducing this function, and using it in place of with authSnaplet in loginUser, allows the code to type check for me. You may need to tweak things a bit to handle your actual instance constraints.


Edit: If the above technique doesn't let you nail down b by some means, and assuming that the types really are intended to be as generic as they're written, then b is impossibly ambiguous and there's no way around it.

Using with authSnaplet eliminates b entirely from the actual type, but leaves it polymorphic with a class constraint on it. This is the same ambiguity that an expression like show . read has, with instance-dependent behavior but no way to pick one.

To avoid this, you have roughly three choices:

  • Retain the ambiguous type explicitly, so that b is found somewhere in the actual type of loginUser, not just the context. This may be undesirable for other reasons in the context of your application.

  • Remove the polymorphism, by only applying with authSnaplet to suitably monomorphic values. If the types are known in advance, there's no room for ambiguity. This potentially means giving up some polymorphism in your handlers, but by breaking things apart you can limit the monomorphism to only code that cares what b is.

  • Make the class constraints themselves unambiguous. If the three type parameters to HasAuth are, in practice, interdependent to some degree such that there will only be one valid instance for any s and u, then a functional dependency from the others to b would be completely appropriate.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top