Какой словарь выбирает GHC, если в области больше одного?

Рассмотрим следующий пример:

import Data.Constraint

class Bar a where
  bar :: a -> a

foo :: (Bar a) => Dict (Bar a) -> a -> a
foo Dict = bar

GHC имеет два варианта использования словаря при выборе экземпляра Bar в foo: он может использовать словарь из ограничения Bar a на foo или он может использовать время выполнения Dict для получить словарь. См. этот вопрос для примера, где словари соответствуют различным экземплярам.

Какой словарь использует GHC и почему он является "правильным" выбором?

Ответы

Ответ 1

GHC просто выбирает один, и это правильный выбор. Любые два словаря для одного и того же ограничения должны быть равны.

OverlappingInstances и IncoherentInstances в основном эквивалентны разрушающей способности; они оба теряют когерентность экземпляра по дизайну (любые два равных ограничения в вашей программе удовлетворяются одним и тем же словарем). OverlappingInstances дает вам немного больше возможностей для разработки, какие экземпляры будут использоваться в каждом конкретном случае, но это не так полезно, когда вы доходите до точки прохождения Dicts в качестве значений первого класса и так далее. Я рассматривал бы только использование OverlappingInstances, когда рассматриваю перекрывающиеся экземпляры, эквивалентные друг другу (например, более эффективную, но в остальном равную реализацию для определенного типа типа Int), но даже тогда, если я достаточно забочусь о производительности, чтобы написать эту специализированную реализацию, Это ошибка производительности, если она не используется, когда она может быть?

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

Теперь верно, что вы можете нарушить последовательность кодов без OverlappingInstances. Фактически вы можете сделать это без сирот и без каких-либо расширений, кроме FlexibleInstances (возможно, проблема в том, что определение "сирота" неверно, когда включено FlexibleInstances). Это очень давняя ошибка GHC, которая частично не устранена из-за того, что (а) она фактически не может вызывать сбои напрямую, насколько известно кому-либо, и (б) может быть много программ, которые фактически полагаются на наличие нескольких экземпляров для одного и того же ограничения в отдельных частях программы, и этого может быть трудно избежать.

Возвращаясь к основной теме, в принципе важно, чтобы GHC мог выбрать любой словарь, доступный для удовлетворения ограничения, потому что, хотя они должны быть равны, GHC может иметь более статическую информацию о некоторых из них, чем другие. Ваш пример немного слишком прост, чтобы быть иллюстративным, но представьте, что вы передали аргумент bar; вообще GHC ничего не знает о словаре, переданном через Dict, поэтому он должен рассматривать это как вызов неизвестной функции, но вы вызывали foo в конкретном типе T, для которого существовал Bar T instance в области видимости, тогда GHC будет знать, что bar из словаря ограничений Bar a был T bar и мог генерировать вызов известной функции и, возможно, встроить T bar и сделать больше оптимизаций.

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

Ответ 2

Он просто выбирает один. Это не правильный выбор; это довольно знаменитая бородавка. Вы можете вызвать аварии таким образом, так что это довольно плохое состояние дел. Вот краткий пример, в котором используется только GADTs, который демонстрирует, что одновременно можно иметь сразу два разных экземпляра:

-- file Class.hs
{-# LANGUAGE GADTs #-}
module Class where

data Dict a where
  Dict :: C a => Dict a

class C a where
  test :: a -> Bool

-- file A.hs
module A where

import Class

instance C Int where
  test _ = True

v :: Dict Int
v = Dict

-- file B.hs
module B where

import Class

instance C Int where
  test _ = False

f :: Dict Int -> Bool
f Dict = test (0 :: Int)

-- file Main.hs
import TestA
import TestB

main = print (f v)

Вы обнаружите, что Main.hs компилируется просто отлично и даже работает. Он печатает True на моей машине с GHC 7.10.1, но это не стабильный результат. Превращение этого в катастрофу оставляют читателю.

Ответ 3

Здесь тест:

{-# LANGUAGE FlexibleInstances, OverlappingInstances, IncoherentInstances #-}
import Data.Constraint

class    C a    where foo :: a -> String
instance C [a]  where foo _ = "[a]"
instance C [()] where foo _ = "[()]"

aDict :: Dict (C [a])
aDict = Dict

bDict :: Dict (C [()])
bDict = Dict

bar1 :: String
bar1 = case (bDict, aDict :: Dict (C [()])) of
         (Dict,Dict) -> foo [()]              -- output: "[a]"

bar2 :: String
bar2 = case (aDict :: Dict (C [()]), bDict) of
         (Dict,Dict) -> foo [()]              -- output: "[()]"

В GHC выше используется "последний" словарь, который был введен в сферу действия. Я бы не стал полагаться на это.

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

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

Короче говоря, некогерентные примеры злы.


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