Когда подходит -XAllowAmbiguousTypes?

Недавно я опубликовал question о синтаксис-2.0 относительно определения share. Я работал в GHC 7.6:

{-# LANGUAGE GADTs, TypeOperators, FlexibleContexts #-}

import Data.Syntactic
import Data.Syntactic.Sugar.BindingT

data Let a where
    Let :: Let (a :-> (a -> b) :-> Full b)

share :: (Let :<: sup,
          sup ~ Domain b, sup ~ Domain a,
          Syntactic a, Syntactic b,
          Syntactic (a -> b),
          SyntacticN (a -> (a -> b) -> b) 
                     fi)
           => a -> (a -> b) -> b
share = sugarSym Let

Однако GHC 7.8 хочет -XAllowAmbiguousTypes скомпилировать эту подпись. В качестве альтернативы, я могу заменить fi на

(ASTF sup (Internal a) -> AST sup ((Internal a) :-> Full (Internal b)) -> ASTF sup (Internal b))

который является типом, подразумеваемым fundep на SyntacticN. Это позволяет мне избежать расширения. Конечно, это

  • очень длинный тип для добавления в уже большую подпись
  • утомительно, чтобы вручную выводить
  • ненужный из-за fundep

Мои вопросы:

  • Это приемлемое использование -XAllowAmbiguousTypes?
  • В общем, когда следует использовать это расширение? Ответ здесь предполагает, что "это почти никогда не является хорошей идеей".
  • Хотя я читал документы, мне все еще трудно решить, является ли ограничение неоднозначным или нет. В частности, рассмотрим эту функцию из Data.Syntactic.Sugar:

    sugarSym :: (sub :<: AST sup, ApplySym sig fi sup, SyntacticN f fi) 
             => sub sig -> f
    sugarSym = sugarN . appSym
    

    Мне кажется, что fi (и, возможно, sup) здесь должен быть неоднозначным, но он компилируется без расширения. Почему sugarSym однозначно, а share -? Поскольку share является приложением sugarSym, ограничения share все поступают прямо из sugarSym.

Ответ 1

Я не вижу опубликованной версии синтаксиса, чья подпись для sugarSym использует эти точные имена типов, поэтому я буду использовать ветку разработки при фиксации 8cfd02 ^, последняя версия, которая все еще использовала эти имена.

Итак, почему GHC жалуется на fi в вашей сигнатуре типа, но не на sugarSym? В документации, с которой вы ссылались, объясняется, что тип является неоднозначным, если он не отображается справа от ограничения, если только ограничение не использует функциональные зависимости для вывода неоднозначного типа из других недвусмысленных типов. Поэтому давайте сравним контексты двух функций и ищем функциональные зависимости.

class ApplySym sig f sym | sig sym -> f, f -> sig sym
class SyntacticN f internal | f -> internal

sugarSym :: ( sub :<: AST sup
            , ApplySym sig fi sup
            , SyntacticN f fi
            ) 
         => sub sig -> f

share :: ( Let :<: sup
         , sup ~ Domain b
         , sup ~ Domain a
         , Syntactic a
         , Syntactic b
         , Syntactic (a -> b)
         , SyntacticN (a -> (a -> b) -> b) fi
         )
      => a -> (a -> b) -> b

Итак, для sugarSym недвусмысленные типы: sub, sig и f, и из них мы должны иметь возможность следить за функциональными зависимостями, чтобы устранить все другие типы, используемые в контексте, а именно sup и fi. И действительно, функциональная зависимость f -> internal в SyntacticN использует наш f для устранения двусмысленности нашего fi, и после этого функциональная зависимость f -> sig sym в ApplySym использует наш недавно неоднозначный fi для устранения неоднозначности supsig, что уже не было двусмысленным). Таким образом, это объясняет, почему sugarSym не требует расширения AllowAmbiguousTypes.

Теперь посмотрим на sugar. Первое, что я замечаю, это то, что компилятор не жалуется на двусмысленный тип, а скорее на перекрывающиеся экземпляры:

Overlapping instances for SyntacticN b fi
  arising from the ambiguity check for ‘share’
Matching givens (or their superclasses):
  (SyntacticN (a -> (a -> b) -> b) fi1)
Matching instances:
  instance [overlap ok] (Syntactic f, Domain f ~ sym,
                         fi ~ AST sym (Full (Internal f))) =>
                        SyntacticN f fi
    -- Defined in ‘Data.Syntactic.Sugar’
  instance [overlap ok] (Syntactic a, Domain a ~ sym,
                         ia ~ Internal a, SyntacticN f fi) =>
                        SyntacticN (a -> f) (AST sym (Full ia) -> fi)
    -- Defined in ‘Data.Syntactic.Sugar’
(The choice depends on the instantiation of ‘b, fi’)
To defer the ambiguity check to use sites, enable AllowAmbiguousTypes

Итак, если я читаю это правильно, не то, что GHC считает, что ваши типы неоднозначны, а скорее, что, проверяя, являются ли ваши типы неоднозначными, GHC столкнулся с другой отдельной проблемой. Затем он говорит вам, что если вы сказали GHC не выполнять проверку двусмысленности, это не столкнулось бы с этой отдельной проблемой. Это объясняет, почему включение AllowAmbiguousTypes позволяет компилировать ваш код.

Однако проблема с перекрывающимися экземплярами остается. Два экземпляра, перечисленные GHC (SyntacticN f fi и SyntacticN (a -> f) ...), перекрываются друг с другом. Как ни странно, похоже, что первая из них должна пересекаться с любым другим экземпляром, который является подозрительным. А что означает [overlap ok]?

Я подозреваю, что Syntactic скомпилирован с OverlappingInstances. И, глядя на код, он действительно делает.

Экспериментируя немного, кажется, что GHC в порядке с перекрывающимися экземплярами, когда ясно, что он строго более общий, чем другой:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}

class Foo a where
  whichOne :: a -> String

instance Foo a where
  whichOne _ = "a"

instance Foo [a] where
  whichOne _ = "[a]"

-- |
-- >>> main
-- [a]
main :: IO ()
main = putStrLn $ whichOne (undefined :: [Int])

Но GHC не в порядке с перекрывающимися экземплярами, когда ни один из них явно лучше подходит, чем другой:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}

class Foo a where
  whichOne :: a -> String

instance Foo (f Int) where  -- this is the line which changed
  whichOne _ = "f Int"

instance Foo [a] where
  whichOne _ = "[a]"

-- |
-- >>> main
-- Error: Overlapping instances for Foo [Int]
main :: IO ()
main = putStrLn $ whichOne (undefined :: [Int])

В вашей сигнатуре типа используется SyntacticN (a -> (a -> b) -> b) fi, и ни SyntacticN f fi, ни SyntacticN (a -> f) (AST sym (Full ia) -> fi) не лучше, чем другие. Если я изменю эту часть вашей сигнатуры типа на SyntacticN a fi или SyntacticN (a -> (a -> b) -> b) (AST sym (Full ia) -> fi), GHC больше не будет жаловаться на перекрытие.

Если бы я был вами, я бы посмотрел на определение этих двух возможных экземпляров и определил, является ли одна из этих двух реализаций той, которая вам нужна.

Ответ 2

Я обнаружил, что AllowAmbiguousTypes очень удобен для использования с TypeApplications. Рассмотрим функцию natVal :: forall n proxy . KnownNat n => proxy n -> Integer из GHC.TypeLits.

Чтобы использовать эту функцию, я мог бы написать natVal (Proxy::Proxy5). Альтернативный стиль - использовать TypeApplications: natVal @5 Proxy. Тип Proxy выводится типом приложения, и его раздражает необходимость писать каждый раз при вызове natVal. Таким образом, мы можем включить AmbiguousTypes и написать:

{-# Language AllowAmbiguousTypes, ScopedTypeVariables, TypeApplications #-}

ambiguousNatVal :: forall n . (KnownNat n) => Integer
ambiguousNatVal = natVal @n Proxy

five = ambiguousNatVal @5 -- no `Proxy ` needed!

Однако обратите внимание, что как только вы идете двусмысленно, вы не сможете вернуться!