Что не так с нынешней системой ограничений GHC Haskell?

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

Например, у edwardk и tekmo возникли проблемы (например, этот комментарий от tekmo).

2 ответа

Решение

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

Простейшим примером этой проблемы является 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 не ограничивает его результат быть 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 Библиотека использует типы очень высокого класса, поэтому я постоянно сталкиваюсь с этой проблемой.

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

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

Правильные обозначения для ограничений и количественных определений в 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 s для типов ранга 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)) может использоваться как отдельное ограничение, но не может быть наложен псевдоним на новый однопараметрический класс типов для *->* типы.

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

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

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

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

{-# 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 такое однопараметрический класс типов? MonadPlus Maybe от 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) для * -> * -> * типы.

Другие вопросы по тегам