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

Предположим, у меня есть параметрический тип, определенный следующим образом:

newtype FancyComplex a b = FancyComplex (a, b)

Я намерен никогда не использовать этот новый тип для любых других параметров, кроме числовых. Я имею в виду, что для любой реализации я знаю, что параметры a и b всегда будут экземпляром Num.

Я прочитал в этом вопросе, что вы можете сделать это: Можно ли использовать ограничение класса типов в определении нового типа?

{-# LANGUAGE RankNTypes #-}
newtype (Num a, Num b) => FancyComplex a b = FancyComplex (a, b)

Однако этого недостаточно. Если я напишу любой класс следующим образом:

class StupidClass x where add :: x -> x -> x

Тогда я смогу написать

instance StupidClass (FancyComplex a b) where
    add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')

Но ни один GHC не скажет мне, что я не соблюдал требование Num. Поэтому я вынужден делать это каждый раз:

instance (Num a, Num b) => StupidClass (FancyComplex a b) where
    add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')

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

Как я могу автоматически и неявно наследовать ограничения из определения нового типа? Это возможно? если нет, то есть ли причина, почему нет?

В настоящее время мой слабый обходной путь - определить псевдоним типа type FancyComplexReqs a b = (Num a, Num b)

Спасибо


person jam    schedule 10.05.2020    source источник
comment
На самом деле написание ограничений типа в newtype считается ошибкой, если я правильно помню (books.google.be/). Написание ограничения полезно, так как оно встречается в подписи instance, а значит, и в документации. Но если вы не ограничиваете свой тип на уровне newtype, он будет ограничен из-за использования (+) в вашем определении.   -  person Willem Van Onsem    schedule 11.05.2020
comment
Но требование newtype может быть сильнее и выражено красивее, чем более слабые и запутанные требования самого экземпляра. Дело в том, что если он определен на уровне newtype, я не вижу причин, по которым его нельзя было бы автоматически наследовать и документировать в любом экземпляре, который его использует. Дело в том, что ограничение newtype может быть довольно сложным и длинным для написания, и это экономит место. Мой пример искусственно построен, чтобы проиллюстрировать мою точку зрения, но в моем коде у меня есть более сложные структуры, каждый раз переписывать ограничения неудобно.   -  person jam    schedule 11.05.2020
comment
Вы можете написать минимальные требования для каждого экземпляра, но это может быть некрасиво. Иногда я бы предпочел написать более ограничительное и единообразное условие для применения к каждому экземпляру. Чтобы все выглядело более структурировано.   -  person jam    schedule 11.05.2020
comment
@WillemVanOnsem Я думаю, что в документе, который вы мне показываете, говорится, что он плохой, именно потому, что в нем нет решения моего вопроса: отсюда и мой вопрос.   -  person jam    schedule 11.05.2020
comment
@jam Вы можете написать минимальные требования для каждого экземпляра, но это может быть некрасиво. Иногда я бы предпочел написать более ограничительное и единообразное условие для применения к каждому экземпляру. Чтобы удовлетворить эту потребность, я лично просто использовал бы предложенный вами обходной путь (type FancyComplexReqs a b = (Num a, Num b)). Вы также можете прикрепить документацию, объясняющую, почему эти ограничения полезны для повсеместного применения.   -  person Ben    schedule 11.05.2020
comment
@jam Я всегда хотел бы, чтобы в экземпляре была некоторая явная аннотация о том, что он использует дополнительные ограничения для переменных своего типа; синоним ограничения сводит это к аккуратному минимуму, делает очевидным, что везде одна и та же группа ограничений, дает мне единую точку управления, если мне нужно изменить общую группу; это как раз мой личный оптимум. Ваш желаемый синтаксис экономит несколько нажатий клавиш при записи каждого экземпляра, но делает их менее четкими для чтения и не дает вам возможного выхода для записи экземпляров, таких как Functor, которые требуют меньше ограничений.   -  person Ben    schedule 11.05.2020
comment
Не рекомендуется ограничивать ваш тип данных. Ограничивающие функции, которые воздействуют на них вместо этого.   -  person Poscat    schedule 11.05.2020


Ответы (2)


Это невозможно реализовать, по крайней мере, без изменения значения newtype:

newtype (Num a, Num b) => FancyComplex a b = FancyComplex (a, b)

instance StupidClass (FancyComplex a b) where
    add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')

В последней строке a+a' нужна функция +, которая является методом Num, поэтому мы должны иметь ее в своем распоряжении. Я вижу только эти варианты:

  1. Функция + хранится внутри значения FancyComplex. Это сработает, но отчет Haskell требует, чтобы этот newtype имел такое же представление пары в памяти. Нет места для дополнительного указателя.

  2. Ограничение Num a, Num b неявно добавляется в определение экземпляра, так как оно нам нужно в реализации. Это может сработать, но не лучше ли было бы прямо сказать об этом? Наличие неявных ограничений затрудняет чтение экземпляра, поскольку существует ограничение, даже если кажется, что его нет.

Теперь есть возможная альтернатива: если вам нужен вариант 1, и вас устраивает другое представление в памяти во время выполнения, используйте вместо него data:

data FancyComplex a b where
   FancyComplex :: (Num a, Num b) => a -> b -> FancyComplex a b

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

person chi    schedule 10.05.2020

Закодируйте ограничение в GADT следующим образом:

{-# LANGUAGE GADTs #-}

data FancyComplex a b where
  FancyComplex :: (Num a, Num b) => a -> b -> FancyComplex a b

class StupidClass x where add :: x -> x -> x

instance StupidClass (FancyComplex a b) where
    add (FancyComplex a b) (FancyComplex a' b') = FancyComplex (a+a') (b+b')

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

person Joseph Sible-Reinstate Monica    schedule 10.05.2020
comment
Но вы также добавили два новых указателя... - person luqui; 11.05.2020
comment
@luqui Я имел в виду стоимость времени, а не стоимость памяти. Я думаю, это не то, что я сказал, хотя. - person Joseph Sible-Reinstate Monica; 11.05.2020