Неоднозначное разрешение экземпляра в Haskell

Введение и пример использования

Здравствуйте! У меня проблема в Haskell. Рассмотрим следующий код

class PolyMonad m1 m2 m3 | m1 m2 -> m3 where
    polyBind :: m1 a -> (a -> m2 b) -> m3 b

который просто объявляет привязку поли Монады. Хорошим примером сценария использования может быть:

newtype Pure a = Pure { fromPure :: a } deriving (Show)

instance PolyMonad Pure Pure Pure where
    polyBind a f = f (fromPure a)

instance PolyMonad Pure IO IO where
    polyBind a f = f (fromPure a)

instance PolyMonad IO Pure IO where
    polyBind a f = (fromPure . f) <$> a

instance PolyMonad IO IO IO where
    polyBind a f = a >>= f

и используя его с -XRebindableSyntax следующим образом:

test = do
    Pure 5
    print "hello"
    Pure ()

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

Проблема

Давайте рассмотрим немного более сложное использование. Я хочу написать класс, похожий на полимонад, который не всегда выводит m3 b, но в некоторых конкретных случаях он выводит m3 (X b) для определенного X. Для простоты предположим, что мы хотим выводить m3 (X b) ТОЛЬКО, когда либо m1, либо m2 был IO.

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

tst1 x = x `polyBind` (\_ -> Pure 0)
tst2 = (Pure 1) `polyBind` (\_ -> Pure 0)
tst3 x y = x `polyBind` (\_ -> y `polyBind` (\_ -> Pure 0))

В любом случае эти функции хорошо компилируются с использованием класса PolyMonad.

Сделайте попытку решить проблему

Одна из попыток:

class PolyMonad2 m1 m2 m3 b | m1 m2 b -> out where
    polyBind2 :: m1 a -> (a -> m2 b) -> out

и, конечно, мы можем легко записать все необходимые экземпляры, например:

instance PolyMonad2 Pure Pure b (Pure b) where
    polyBind2 a f = f (fromPure a)

instance PolyMonad2 Pure IO b (IO (X b)) where
    polyBind2 a f = fmap X $ f (fromPure a)

-- ...

но наши тестовые функции не будут компилироваться при использовании polyBind2 вместо polyBind. Первая функция (tst1 x = x polyBind2 (\_ -> Pure 0)) выводит ошибку компиляции:

Could not deduce (PolyMonad2 m1 Pure b0 out)
  arising from the ambiguity check for ‘tst1’
from the context (PolyMonad2 m1 Pure b out, Num b)
  bound by the inferred type for ‘tst1’:
             (PolyMonad2 m1 Pure b out, Num b) => m1 a -> out
  at /tmp/Problem.hs:51:1-37
The type variable ‘b0’ is ambiguous
When checking that ‘tst1’
  has the inferred type ‘forall (m1 :: * -> *) b out a.
                         (PolyMonad2 m1 Pure b out, Num b) =>
                         m1 a -> out’
Probable cause: the inferred type is ambiguous

Семейство закрытых типов пытается решить проблему

Несколько лучше будет использовать closed type families здесь, например:

class PolyMonad3 m1 m2 where
    polyBind3 :: m1 a -> (a -> m2 b) -> OutputOf m1 m2 b

type family OutputOf m1 m2 a where
    OutputOf Pure Pure a = Pure a
    OutputOf x    y    a = Pure (X a)

но затем, пытаясь скомпилировать функцию tst1 (tst1 x = x polyBind3 (\_ -> Pure 0)), мы получаем другую ошибку времени компиляции:

Could not deduce (OutputOf m1 Pure b0 ~ OutputOf m1 Pure b)
from the context (PolyMonad3 m1 Pure, Num b)
  bound by the inferred type for ‘tst1’:
             (PolyMonad3 m1 Pure, Num b) => m1 a -> OutputOf m1 Pure b
  at /tmp/Problem.hs:59:1-37
NB: ‘OutputOf’ is a type function, and may not be injective
The type variable ‘b0’ is ambiguous
Expected type: m1 a -> OutputOf m1 Pure b
  Actual type: m1 a -> OutputOf m1 Pure b0
When checking that ‘tst1’
  has the inferred type ‘forall (m1 :: * -> *) a b.
                         (PolyMonad3 m1 Pure, Num b) =>
                         m1 a -> OutputOf m1 Pure b’
Probable cause: the inferred type is ambiguous

Хакерная попытка сделать это вокруг

Я нашел другое решение, но взломанное и, в конце концов, не работало. Но это очень интересно. Рассмотрим следующий класс типа:

class PolyMonad4 m1 m2 b out | m1 m2 b -> out, out -> b where
    polyBind4 :: m1 a -> (a -> m2 b) -> out

Конечно, функциональная зависимость out -> b просто неверна, потому что мы не можем определить такие экземпляры, как:

instance PolyMonad4 Pure IO b (IO (X b)) where
    polyBind4 a f = fmap X $ f (fromPure a)

instance PolyMonad4 IO IO b (IO b) where
    polyBind4 = undefined

но разрешите играть с ним и объявить их так (используя -XUndecidableInstances):

instance out~(Pure b) => PolyMonad4 Pure Pure b out where
    polyBind4 a f = f (fromPure a)

instance out~(IO(X b)) => PolyMonad4 Pure IO b out where
    polyBind4 a f = fmap X $ f (fromPure a)

instance out~(IO b) => PolyMonad4 IO IO b out where
    polyBind4 = undefined

instance out~(IO(X b)) => PolyMonad4 IO Pure b out where
    polyBind4 = undefined

Что смешно, некоторые из наших тестовых функций компилируются и работают, а именно:

tst1' x = x `polyBind4` (\_ -> Pure 0)
tst2' = (Pure 1) `polyBind4` (\_ -> Pure 0)

но этого нет:

tst3' x y = x `polyBind4` (\_ -> y `polyBind4` (\_ -> Pure 0))

что приводит к ошибке времени компиляции:

Could not deduce (PolyMonad4 m3 Pure b0 (m20 b))
  arising from the ambiguity check for ‘tst3'’
from the context (PolyMonad4 m3 Pure b1 (m2 b),
                  PolyMonad4 m1 m2 b out,
                  Num b1)
  bound by the inferred type for ‘tst3'’:
             (PolyMonad4 m3 Pure b1 (m2 b), PolyMonad4 m1 m2 b out, Num b1) =>
             m1 a -> m3 a1 -> out
  at /tmp/Problem.hs:104:1-62
The type variables ‘m20’, ‘b0’ are ambiguous
When checking that ‘tst3'’
  has the inferred type ‘forall (m1 :: * -> *)
                                (m2 :: * -> *)
                                b
                                out
                                a
                                (m3 :: * -> *)
                                b1
                                a1.
                         (PolyMonad4 m3 Pure b1 (m2 b), PolyMonad4 m1 m2 b out, Num b1) =>
                         m1 a -> m3 a1 -> out’
Probable cause: the inferred type is ambiguous

Еще более хакерская попытка использования обертки newtype

Я сказал, что он еще более взломан, потому что он приводит нас к использованию -XIncoherentInstances, которые являются Just (Pure evil). Одной из идей может быть, конечно, написать оболочку newtype:

newtype XWrapper m a = XWrapper (m (X (a)))

и некоторые утилиты для его распаковки:

class UnpackWrapper a b | a -> b where
    unpackWrapper :: a -> b

instance UnpackWrapper (XWrapper m a) (m (X a)) where
    unpackWrapper (XWrapper a) = a

instance UnpackWrapper (Pure a) (Pure a) where
    unpackWrapper = id

instance UnpackWrapper (IO a) (IO a) where
    unpackWrapper = id

теперь мы можем легко объявить следующие экземпляры:

instance PolyMonad Pure Pure Pure 
instance PolyMonad Pure IO (XWrapper IO) 
instance PolyMonad IO Pure (XWrapper IO) 
instance PolyMonad IO IO IO 

но опять же, мы не можем запускать наши тесты при объединении функций bind и разворота:

polyBindUnwrap a f = unpackWrapper $ polyBind a f

тестовые функции не скомпилируются снова. Мы можем работать здесь с некоторыми -XIncoherentInstances (см. Список кодов в конце), но пока я не получил никаких хороших результатов.

Последний вопрос

Это проблема, которая не может быть выполнена с использованием текущей реализации GHC Haskell?

Полный список кодов

Вот полный список кодов, который можно запустить в GHC >= 7.8:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

import Control.Applicative

class PolyMonad m1 m2 m3 | m1 m2 -> m3 where
    polyBind :: m1 a -> (a -> m2 b) -> m3 b


----------------------------------------------------------------------
-- Some utils
----------------------------------------------------------------------

newtype Pure a = Pure { fromPure :: a } deriving (Show)
newtype X a = X { fromX :: a } deriving (Show)

main = return ()

----------------------------------------------------------------------
-- Example use cases
----------------------------------------------------------------------

instance PolyMonad Pure Pure Pure where
    polyBind a f = f (fromPure a)

instance PolyMonad Pure IO IO where
    polyBind a f = f (fromPure a)

instance PolyMonad IO Pure IO where
    polyBind a f = (fromPure . f) <$> a

instance PolyMonad IO IO IO where
    polyBind a f = a >>= f

-- works when using rebindable syntax
--test = do
--    Pure 5
--    print "hello"
--    Pure ()

tst1 x = x `polyBind` (\_ -> Pure 0)
tst2 = (Pure 1) `polyBind` (\_ -> Pure 0)
tst3 x y = x `polyBind` (\_ -> y `polyBind` (\_ -> Pure 0))

----------------------------------------------------------------------
-- First attempt to solve the problem
----------------------------------------------------------------------


class PolyMonad2 m1 m2 b out | m1 m2 b -> out where
    polyBind2 :: m1 a -> (a -> m2 b) -> out


instance PolyMonad2 Pure Pure b (Pure b) where
    polyBind2 a f = f (fromPure a)

instance PolyMonad2 Pure IO b (IO (X b)) where
    polyBind2 a f = fmap X $ f (fromPure a)

-- ...

-- tst1 x = x `polyBind2` (\_ -> Pure 0) -- does NOT compile


----------------------------------------------------------------------
-- Second attempt to solve the problem
----------------------------------------------------------------------

class PolyMonad3 m1 m2 where
    polyBind3 :: m1 a -> (a -> m2 b) -> OutputOf m1 m2 b

type family OutputOf m1 m2 a where
    OutputOf Pure Pure a = Pure a
    OutputOf x    y    a = Pure (X a)

-- tst1 x = x `polyBind3` (\_ -> Pure 0) -- does NOT compile


----------------------------------------------------------------------
-- Third attempt to solve the problem
----------------------------------------------------------------------

class PolyMonad4 m1 m2 b out | m1 m2 b -> out, out -> b where
    polyBind4 :: m1 a -> (a -> m2 b) -> out


instance out~(Pure b) => PolyMonad4 Pure Pure b out where
    polyBind4 a f = f (fromPure a)

instance out~(IO(X b)) => PolyMonad4 Pure IO b out where
    polyBind4 a f = fmap X $ f (fromPure a)

instance out~(IO b) => PolyMonad4 IO IO b out where
    polyBind4 = undefined

instance out~(IO(X b)) => PolyMonad4 IO Pure b out where
    polyBind4 = undefined


tst1' x = x `polyBind4` (\_ -> Pure 0)
tst2' = (Pure 1) `polyBind4` (\_ -> Pure 0)
--tst3' x y = x `polyBind4` (\_ -> y `polyBind4` (\_ -> Pure 0)) -- does NOT compile


----------------------------------------------------------------------
-- Fourth attempt to solve the problem
----------------------------------------------------------------------

class PolyMonad6 m1 m2 m3 | m1 m2 -> m3 where
    polyBind6 :: m1 a -> (a -> m2 b) -> m3 b

newtype XWrapper m a = XWrapper (m (X (a)))


class UnpackWrapper a b | a -> b where
    unpackWrapper :: a -> b

instance UnpackWrapper (XWrapper m a) (m (X a)) where
    unpackWrapper (XWrapper a) = a

instance UnpackWrapper (Pure a) (Pure a) where
    unpackWrapper = id

instance UnpackWrapper (IO a) (IO a) where
    unpackWrapper = id

--instance (a1~a2, out~(m a2)) => UnpackWrapper (m a1) out where
--    unpackWrapper = id


--{-# LANGUAGE OverlappingInstances #-}
--{-# LANGUAGE IncoherentInstances #-}

instance PolyMonad6 Pure Pure Pure where
    polyBind6 = undefined

instance PolyMonad6 Pure IO (XWrapper IO) where
    polyBind6 = undefined

instance PolyMonad6 IO Pure (XWrapper IO) where
    polyBind6 = undefined

instance PolyMonad6 IO IO IO where
    polyBind6 = undefined

--polyBind6' a f = unpackWrapper $ polyBind6 a f

--tst1'' x = x `polyBind6'` (\_ -> Pure 0)
--tst2'' = (Pure 1) `polyBind4` (\_ -> Pure 0)
--tst3'' x y = x `polyBind4` (\_ -> y `polyBind4` (\_ -> Pure 0)) -- does NOT compile

Ответ 1

Я не думаю, что этот вопрос зависит от семейств инъективных типов.

Бит семейства инъективных типов упоминается в сообщении об ошибке вокруг семейства закрытого типа OutputOf. Но эта функция действительно не является инъективной - второе уравнение для нее допускает любые x и y. GHC любит напоминать пользователям о том, что он не анализирует эффективность инъекций для семейств типов, но иногда (например, здесь) это предупреждение не помогает.

Вместо этого проблемы, с которыми вы сталкиваетесь, похоже, связаны с перегруженными числами. Когда вы говорите Pure 0, GHC правильно вводит тип Num a => Pure a. Проблема в том, что функции типа уровня, к которым вы обращаетесь (тип разрешения класса, функциональные зависимости, семейства типов), очень сильно заботятся о том, какой конкретный выбор для a здесь. Например, вполне возможно, что любой из ваших подходов ведет себя по-разному для Int, чем для Integer. (Например, у вас могут быть разные экземпляры PolyMonad2 или дополнительные уравнения в OutputOf.)

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

Ответ 2

Я считаю, что фундаментальное различие заключается в том, что здесь:

class PolyMonad m1 m2 m3 | m1 m2 -> m3 where
    polyBind :: m1 a -> (a -> m2 b) -> m3 b

b полностью полиморфна; это не параметр класса типа, поэтому можно выбрать экземпляр и функциональную зависимость, применяемую для определения m3 от m1 и m2 независимо от b. Он также появляется в двух местах; если тип inferencer знает тип результата или тип функции, переданной в polyBind, тогда он может достаточно определить b. И такой тип, как Num b => b, будет "пропускать" многие приложения polyBind до тех пор, пока он не будет использоваться в месте, которое фиксирует конкретный тип. Хотя я думаю, что это может быть просто ограничение мономорфизма, по умолчанию использующее тип, который избавляет вас от неоднозначной ошибки переменной типа в этом случае (именно то, что она была предназначена для выполнения).

Если здесь:

class PolyMonad2 m1 m2 m3 b | m1 m2 b -> out where
    polyBind2 :: m1 a -> (a -> m2 b) -> out

b появляется как параметр класса типа. Если мы попытаемся сделать вывод, что out, нам нужно полностью определить b, прежде чем мы сможем выбрать экземпляр. И нет причин для b нести какое-либо конкретное отношение к структуре типа out (или, скорее, это отношение может быть различным для каждого отдельного экземпляра, что в конечном счете является тем, чего вы пытаетесь достичь), поэтому невозможно "следовать за b через" цепочку вызовов polyBind2, если вы полностью не разрешили все экземпляры.

И если b является полиморфным числом Num b => b, а out ограничено его использованием формы Num c => m c (для некоторого конструктора типов m), нет причин, чтобы c и b должны быть одним и тем же экземпляром Num. Таким образом, в цепочке цепочек вызовов polyBind2, работающих по номерам, каждый промежуточный результат может использовать другой экземпляр Num, и, не зная ни одного из них, нет способа выбрать правильные экземпляры PolyMonad2, которые объединяют b с чем-то в out. Тип defaulting применяется только в том случае, если все ограничения для переменной являются числовыми классами прелюдии, но здесь b участвует в ограничении PolyMonad2 m1 m2 m3 b, поэтому его нельзя дефолтировать (что, вероятно, хорошо, поскольку именно то, вы можете повлиять на то, какой экземпляр используется и резко изменить поведение программы, это только числовые классы, которые, как известно, являются "приближениями" друг друга, так что если программа неоднозначна в отношении того, какой экземпляр использовать, то она полуразмерна просто произвольно выбирайте один, а не жалуйтесь на двусмысленность).

То же самое верно для любого метода определения out из m1, m2 и b, насколько я знаю, будь то функциональные зависимости, типы семейств или что-то еще. Я не уверен, как на самом деле решить эту проблему здесь, но не предоставляя больше аннотаций типа.