Я не понимаю, почему этот код компилируется с подсказками типов, но не компилируется без них. Не должно быть двусмысленностей экземпляра (есть один экземпляр).

class Monad m => FcnDef β m | β -> m where
    def :: String -> β -- takes a name

instance Monad m => FcnDef (m α -> m α) m where
    def s body = body

dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" ((return ()) :: m ())

С другой стороны, если опустить :: m () или все объявления типов, компиляция завершится с ошибкой,

No instance for (FcnDef (m0 () -> t0) m0)
  arising from a use of `def'

Для пояснения, код пытается создать многовариантный тип для def, поэтому можно написать, например,

def "dummy2" "input" $ \in -> return ()

Редактировать

Этот вопрос более интересен, чем вопрос об отсутствии мономорфизма. Если добавить такой код, то экземпляры будут преобразованы в конкретные типы, а именно

dummyTest = def "dummy" (return ())
g :: IO ()
g = dummyTest

Компиляция также не выполняется.

5
gatoatigrado 29 Авг 2011 в 01:34

2 ответа

Лучший ответ

Необходимость в сигнатуре внешнего типа вызвана ограничением мономорфизма.

Раздача за это - левая часть вашего определения.

dummyTest = ...

Поскольку это определение не имеет аргументов, компилятор попытается сделать определение мономорфным. Добавление сигнатуры типа отменяет это поведение.

Однако, как вы отметили, этого недостаточно. По какой-то причине компилятор не может определить тип внутреннего выражения. Почему? Давайте разберемся. Пора поиграть в механизм вывода типов!

Начнем с внешнего типа и попробуем определить тип внутреннего выражения.

dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" (return ())

Тип def - FcnDef β m => String -> β, но здесь мы применили def к двум аргументам. Это говорит нам, что β должен быть типом функции. Назовем это x -> y.

Затем мы можем легко вывести, что y должно быть равно m (), чтобы удовлетворить внешний тип. Кроме того, тип аргумента return () - Monad m1 => m1 (), поэтому мы можем сделать вывод, что тип def, который мы ищем, должен иметь тип FcnDef (m1 () -> m ()) m0 => def :: String -> m1 () -> m ().

Затем мы перейдем к поиску используемого экземпляра. Единственный доступный экземпляр недостаточно общий , так как требует, чтобы m1 и m были одинаковыми. Поэтому мы громко жалуемся на такое сообщение:

Could not deduce (FcnDef (m1 () -> m ()) m0)
  arising from a use of `def'
from the context (Monad m)
  bound by the type signature for dummyTest :: Monad m => m ()
  at FcnDef.hs:10:1-51
Possible fix:
  add (FcnDef (m1 () -> m ()) m0) to the context of
    the type signature for dummyTest :: Monad m => m ()
  or add an instance declaration for (FcnDef (m1 () -> m ()) m0)
In the expression: def "dummy" ((return ()))
In an equation for `dummyTest':
    dummyTest = def "dummy" ((return ()))

Ключевым моментом здесь является то, что тот факт, что конкретный экземпляр случайно валяется, не повлиял на наш выбор, когда мы пытались вывести тип.

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

class Monad m => FcnDef β m | β -> m where
    def :: String -> β -> β

instance Monad m => FcnDef (m α) m where
    def s body = body

-- dummyTest :: forall m. Monad m => m ()
dummyTest = def "dummy" (return ())

Теперь механизм вывода типов может легко определить, что монада return должна быть такой же, как монада в результате, и, используя NoMonomorphismRestriction, мы также можем отбросить внешнюю сигнатуру типа.

Конечно, я не уверен на 100%, чего вы здесь пытаетесь достичь, поэтому вам придется судить, имеет ли это смысл в контексте того, что вы пытаетесь сделать.

7
hammar 28 Авг 2011 в 22:46

Как отметил @pigworker в комментарии:

Вывод экземпляра является защитным по отношению к возможным будущим экземплярам. Похоже, что в заголовке вашего экземпляра один и тот же var дважды. Он не сработает, если что-то еще не заставит эти типы быть одинаковыми.

Это действительно основная проблема, и хотя подход @hammar к добавлению дополнительного параметра, вероятно, является наиболее полезным решением, в подобном случае мы можем сделать наблюдение, а затем исправить слабость механизм выбора экземпляра в нашу пользу. Для начала обратите внимание, что мы не можем написать два таких экземпляра:

instance Monad m => FcnDef (m α -> m α) m where -- etc...
instance Monad m => FcnDef (m1 α -> m2 b) m3 where -- etc...

Почему нет? Несколько извращенно, потому что они слишком перекрываются . Разные переменные типа во втором случае, конечно, могут быть разумно созданы с одними и теми же типами и, таким образом, соответствовать первому экземпляру; и расширение OverlappingInstances здесь не поможет.

Учитывая, что в данном случае нужен первый экземпляр, и что перекрытие означает, что мы никогда не сможем добавить второй экземпляр, мы можем быстро вытащить один на GHC. Мы начнем с написания второго универсального экземпляра, но затем мы вставим что-нибудь в контекст (который будет исследован только потом!), Который принудительно унифицирует типы. Если вы включите TypeFamilies, вы можете использовать здесь ~ для хорошего эффекта и напишите следующее:

instance (m1 ~ m2, m2 ~ m3, a ~ b, Monad m) => FcnDef (m1 α -> m2 b) m3 where -- etc...

Здесь произойдет то, что GHC выберет здесь универсальный экземпляр независимо от типов, а затем попытается объединить их, чтобы удовлетворить ограничению. Если они не могут объединиться, вы получите ошибку проверки типа, как и ожидалось. Если они совпадают, все хорошо. Но важная часть состоит в том, что, в отличие от вашего кода, если они могут унифицироваться, но остаются неоднозначными, это заставит их объединить.

В большинстве случаев очень неприятно, что выбор экземпляра игнорирует контекст, но на самом деле это очень полезно в этом сценарии.

3
C. A. McCann 30 Авг 2011 в 13:57