Что случилось с системой ограничения текущих ограничений GHC Haskell?

Я слышал, что есть некоторые проблемы с Haskell "сломанной" системой ограничений, начиная с GHC 7.6 и ниже. Что "неправильно" с ним? Существует ли сопоставимая существующая система, которая преодолевает эти недостатки?

Например, как edwardk, так и tekmo столкнулись с трудностями (например, этот комментарий от tekmo).

Ответ 1

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

Простейшим примером этой проблемы является класс MonadPlus, определяемый как:

class MonadPlus m where
    mzero :: m a
    mplus :: m a -> m a -> m a

... со следующими законами:

mzero `mplus` m = m

m `mplus` mzero = m

(m1 `mplus` m2) `mplus` m3 = m1 `mplus` (m2 `mplus` m3)

Обратите внимание, что это законы Monoid, где класс Monoid задается:

class Monoid a where
    mempty :: a
    mappend :: a -> a -> a

mempty `mplus` a = a

a `mplus` mempty = a

(a1 `mplus` a2) `mplus` a3 = a1 `mplus` (a2 `mplus` a3)

Итак, почему у нас даже есть класс MonadPlus? Причина в том, что Haskell запрещает нам писать ограничения формы:

(forall a . Monoid (m a)) => ...

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

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

(forall a' a b' b . Monad (p a a' b' b m)) => ...

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

Это также возникает при создании трансформаторов, как монадных трансформаторов, так и прокси-трансформаторов, которые я включаю в свою библиотеку. Мы хотели бы написать что-то вроде:

data Compose t1 t2 m r = C (t1 (t2 m) r)

instance (MonadTrans t1, MonadTrans t2) => MonadTrans (Compose t1 t2) where
    lift = C . lift . lift

Эта первая попытка не работает, потому что lift не ограничивает ее результат a Monad. Нам действительно нужно:

class (forall m . Monad m => Monad (t m)) => MonadTrans t where
    lift :: (Monad m) => m r -> t m r

... но система ограничений Haskell не позволяет этого.

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

class SomeClass someHigherKindedTypeConstructor where
    ...

... но вы захотите ограничить некоторый конструктор производного типа нижнего рода:

class (SomeConstraint (someHigherKindedTypeConstructor a b c))
    => SomeClass someHigherKindedTypeConstructor where
    ...

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

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

Ответ 2

[ответ на вопрос Габриэля Гонсалеса]

Правильная нотация ограничений и квантификаций в Haskell следующая:

<functions-definition> ::= <functions> :: <quantified-type-expression>

<quantified-type-expression> ::= forall <type-variables-with-kinds> . (<constraints>) => <type-expression>

<type-expression> ::= <type-expression> -> <quantified-type-expression>
                    | ...

...

Виды могут быть опущены, а также forall для типов ранга 1:

<simply-quantified-type-expression> ::= (<constraints-that-uses-rank-1-type-variables>) => <type-expression>

Например:

{-# LANGUAGE Rank2Types #-}

msum :: forall m a. Monoid (m a) => [m a] -> m a
msum = mconcat

mfilter :: forall m a. (Monad m, Monoid (m a)) => (a -> Bool) -> m a -> m a
mfilter p ma = do { a <- ma; if p a then return a else mempty }

guard :: forall m. (Monad m, Monoid (m ())) => Bool -> m ()
guard True = return ()
guard False = mempty

или без Rank2Types (так как здесь мы имеем только типы ранга-1) и используя CPP (j4f):

{-# LANGUAGE CPP #-}

#define MonadPlus(m, a) (Monad m, Monoid (m a))

msum :: MonadPlus(m, a) => [m a] -> m a
msum = mconcat

mfilter :: MonadPlus(m, a) => (a -> Bool) -> m a -> m a
mfilter p ma = do { a <- ma; if p a then return a else mempty }

guard :: MonadPlus(m, ()) => Bool -> m ()
guard True = return ()
guard False = mempty

"Проблема" заключается в том, что мы не можем писать

class (Monad m, Monoid (m a)) => MonadPlus m where
  ...

или

class forall m a. (Monad m, Monoid (m a)) => MonadPlus m where
  ...

То есть, forall m a. (Monad m, Monoid (m a)) может использоваться как автономное ограничение, но не может быть псевдонимом с новым однопараметрическим классом типа *->*.

Это связано с тем, что механизм детекции typeclass работает следующим образом:

class (constraints[a, b, c, d, e, ...]) => ClassName (a b c) (d e) ...

то есть. сторона rhs вводит переменные типа, а не lhs или forall в lhs.

Вместо этого нам нужно написать 2-параметрическое typeclass:

{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts, FlexibleInstances #-}

class (Monad m, Monoid (m a)) => MonadPlus m a where
  mzero :: m a
  mzero = mempty
  mplus :: m a -> m a -> m a
  mplus = mappend

instance MonadPlus [] a
instance Monoid a => MonadPlus Maybe a

msum :: MonadPlus m a => [m a] -> m a
msum = mconcat

mfilter :: MonadPlus m a => (a -> Bool) -> m a -> m a
mfilter p ma = do { a <- ma; if p a then return a else mzero }

guard :: MonadPlus m () => Bool -> m ()
guard True = return ()
guard False = mzero

Минусы: нам нужно указывать второй параметр каждый раз, когда мы используем MonadPlus.

Вопрос: как

instance Monoid a => MonadPlus Maybe a

может быть записано, если MonadPlus является однопараметрическим typeclass? MonadPlus Maybe from base:

instance MonadPlus Maybe where
   mzero = Nothing
   Nothing `mplus` ys  = ys
   xs      `mplus` _ys = xs

работает не как Monoid Maybe:

instance Monoid a => Monoid (Maybe a) where
  mempty = Nothing
  Nothing `mappend` m = m
  m `mappend` Nothing = m
  Just m1 `mappend` Just m2 = Just (m1 `mappend` m2) -- < here

:

(Just [1,2] `mplus` Just [3,4]) `mplus` Just [5,6] => Just [1,2]
(Just [1,2] `mappend` Just [3,4]) `mappend` Just [5,6] => Just [1,2,3,4,5,6]

Аналогично, forall m a b n c d e. (Foo (m a b), Bar (n c d) e) порождает (7 - 2 * 2) -параметрический тип, если мы хотим, чтобы типы *, (7 - 2 * 1) -параметрические типы для типов * -> *, и (7 - 2 * 0) для типов * -> * -> *.