Переменные неоднозначного типа для зависимых ограничений класса

Я пишу новую систему аутентификации для веб-фреймворка Snap, потому что встроенная недостаточно модульная, и у него есть некоторые функции, которые являются избыточными / «мертвым грузом» для моего приложения. Однако эта проблема вообще не связана со Snap.

При этом я столкнулся с проблемой неоднозначных ограничений типа. В следующем коде мне кажется очевидным, что тип back может быть только переменной типа b в типе функций, но GHC жалуется, что тип неоднозначен.

Как я могу изменить следующий код, чтобы тип back был b, без использования, например. ScopedTypeVariables (потому что проблема связана с ограничением, а не со слишком общими типами)? Есть ли где-то нужная функциональная зависимость?

Соответствующие классы типов:

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))

Код, который не работает:

-- 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

Полная ошибка:

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;
               ... }

person dflemstr    schedule 17.12.2011    source источник
comment
Ведущие символы подчеркивания в Haskell обычно обозначают безразличное значение. Насколько я знаю, это чисто стилистически (компилятор просто не предупреждает о неиспользуемых значениях), но я бы не стал использовать их в качестве функций-аксессоров.   -  person Paul Johnson    schedule 17.12.2011
comment
AuthBackend — это класс многопараметрического типа. Возможно, вам нужна функциональная зависимость, например, класс [...] AuthBackend b u | у -> б. Это означает, что для любого типа u может быть только один соответствующий тип b, являющийся экземпляром этого класса.   -  person Paul Johnson    schedule 17.12.2011
comment
@Paul Johnson: Ведущие символы подчеркивания - это то, как вы получаете линзы, используя data-lens-template. Они никогда не используются напрямую. Кроме того, я думаю, что отказ от предупреждения относится только к параметрам с такими именами.   -  person ehird    schedule 17.12.2011
comment
Ведущие символы подчеркивания @PaulJohnson являются обязательным требованием для функции makeLens TH. Версии с подчеркиванием никогда не используются, за исключением внутренних линз. И я подумал, что это будет зависимость, но я хочу, чтобы классы были как можно более общими, и не вводить функциональные зависимости, если они не нужны, поэтому я не хочу вставлять зависимости везде, если есть другое решение.   -  person dflemstr    schedule 17.12.2011
comment
Начальные версии подчеркивания иногда бывают полезны, когда вам нужна функция, которую они представляют. В вашем примере я, конечно, могу представить себе использование _activeUser :: AuthSnaplet b u -> Maybe u.   -  person mightybyte    schedule 19.12.2011
comment
@mightybyte, поскольку Snaplet находится в монаде состояния, вам почти никогда не нужно использовать геттер напрямую (из-за data-lens-fd), и для этих нескольких ситуаций предпочтительнее будет getL activeUser, поскольку вам не нужно экспортировать конструктор типа данных тогда.   -  person dflemstr    schedule 19.12.2011


Ответы (1)


Рискну предположить, что корень вашей проблемы в выражении with authSnaplet. Вот почему:

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

Не обращайте внимания на контекст, я заполнил несколько фиктивных экземпляров только для того, чтобы загрузить что-то в GHCi. Обратите внимание на переменные типа здесь — много неоднозначности, и по крайней мере две из них, как я ожидаю, должны быть одного и того же типа. Вероятно, самый простой способ справиться с этим — создать небольшую вспомогательную функцию с сигнатурой типа, которая немного сужает ситуацию, например:

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

Опять же, простите за ерунду, на самом деле у меня сейчас не установлен Snap, что делает вещи неудобными. Внедрение этой функции и использование ее вместо with authSnaplet в loginUser позволяет коду проверять тип за меня. Возможно, вам придется немного изменить настройки, чтобы справиться с вашими реальными ограничениями экземпляра.


Редактировать: Если описанный выше метод не позволяет каким-то образом зафиксировать b и предположить, что типы действительно предназначены для того, чтобы быть такими же общими, как они написаны, то b невозможно двусмысленно, и есть никак не обойти это.

Использование with authSnaplet полностью исключает b из фактического типа, но оставляет его полиморфным с ограничением класса. Это та же двусмысленность, что и выражение, подобное show . read, с поведением, зависящим от экземпляра, но без возможности выбора.

Чтобы избежать этого, у вас есть примерно три варианта:

  • Явно сохраните неоднозначный тип, чтобы b находился где-то в фактическом типе loginUser, а не только в контексте. Это может быть нежелательно по другим причинам в контексте вашего приложения.

  • Удалите полиморфизм, применив with authSnaplet только к подходящим мономорфным значениям. Если типы известны заранее, двусмысленности быть не может. Это потенциально означает отказ от некоторого полиморфизма в ваших обработчиках, но, разбивая вещи на части, вы можете ограничить мономорфизм только тем кодом, который заботится о том, что такое b.

  • Сделайте сами ограничения класса однозначными. Если три параметра типа для HasAuth на практике до некоторой степени взаимозависимы, так что будет только один допустимый экземпляр для любых s и u, то функциональная зависимость от других к b будет вполне уместной.

person C. A. McCann    schedule 17.12.2011
comment
Это не устраняло неоднозначность b. Реальный тип with authSnaplet — это (HasAuth s b u, MonadSnaplet m) => m a (AuthSnaplet b u) r -> m a s r, и введение withAuthSnaplet :: HasAuth s b u => Handler a (AuthSnaplet b u) (Either AuthFailure u) -> Handler a s (Either AuthFailure u) не изменило двусмысленности. - person dflemstr; 17.12.2011
comment
@dflemstr: По крайней мере, это устранило двусмысленность в u? - person C. A. McCann; 17.12.2011
comment
Добавление зависимости от AuthBackend b u | u -> b решило последнюю проблему, и теперь я понимаю, почему это необходимо. Я не понимаю, почему специализация withAuthSnaplet способствовала решению, хотя... - person dflemstr; 17.12.2011
comment
@dflemstr: гарантирует, что u в Either AuthFailure u совпадает с u в AuthSnaplet b u. В противном случае последнее имело бы ту же неоднозначность, что и b, но поскольку оно появляется в типе loginUser, ограничение можно распространять легче. В основном та же идея, что и в первом варианте, который я упомянул для b. - person C. A. McCann; 17.12.2011