Подтвердить что ты не робот

Почему ограничения на данные - это плохо?

Я знаю, что этот вопрос задан и ответил много раз, но я до сих пор не понимаю, почему помехи ограничениям на тип данных - это плохо.

Например, возьмите Data.Map k a. Для всех полезных функций, связанных с Map, требуется ограничение Ord k. Таким образом, существует неявное ограничение на определение Data.Map. Почему лучше держать его неявным, а не позволять компилятору и программистам знать, что Data.Map нужен заказный ключ.

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

Например, я могу написать

data User = User { name :: String }

и это приемлемо. Однако это не ограниченная версия

data User' s = User' { name :: s }

После того, как все 99% функций, которые я напишу для типа User, не нуждаются в String, и немногие, для которых, вероятно, потребуется s быть IsString и Show.

Итак, почему слабая версия User считается плохой:

data (IsString s, Show s, ...) => User'' { name :: s }

в то время как оба User и User' считаются хорошими?

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

Update

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

4b9b3361

Ответ 1

TL; DR:
Используйте GADT для предоставления неявных контекстов данных.
Не используйте какое-либо ограничение данных, если вы можете делать с экземплярами Functor и т.д.
Карта слишком старая, чтобы в любом случае перейти на GADT. Прокрутите страницу вниз, если вы хотите увидеть реализацию User с помощью GADT


Позвольте использовать тематическое исследование сумки, где все, о чем мы заботимся, - это то, сколько раз в ней что-то есть. (Как неупорядоченная последовательность. Нам почти всегда нужно ограничить Eq, чтобы делать с ней что-нибудь полезное.

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

GADTs - решение проблемы с ограничениями данных

Простой способ сделать то, что вам нужно, - использовать GADT:

Обратите внимание, что ограничение Eq не только заставляет вас использовать типы с экземпляром Eq при создании GADTBags, но и предоставляет этот экземпляр неявно везде, где появляется конструктор GADTBag. Поэтому для count не нужен контекст Eq, тогда как countV2 does - он не использует конструктор:

{-# LANGUAGE GADTs #-}

data GADTBag a where
   GADTBag :: Eq a => [a] -> GADTBag a
unGADTBag (GADTBag xs) = xs

instance Show a => Show (GADTBag a) where
  showsPrec i (GADTBag xs) = showParen (i>9) (("GADTBag " ++ show xs) ++)

count :: a -> GADTBag a -> Int -- no Eq here
count a (GADTBag xs) = length.filter (==a) $ xs  -- but == here

countV2 a = length.filter (==a).unGADTBag

size :: GADTBag a -> Int
size (GADTBag xs) = length xs
ghci> count 'l' (GADTBag "Hello")
2
ghci> :t countV2
countV2 :: Eq a => a -> GADTBag a -> Int

Теперь нам не понадобилось ограничение Eq, когда мы нашли общий размер мешка, но он все равно не загромождал наше определение. (Мы могли бы использовать size = length . unGADTBag так же хорошо.)

Теперь сделаем функтор:

instance Functor GADTBag where
  fmap f (GADTBag xs) = GADTBag (map f xs)

упс!

DataConstraints_so.lhs:49:30:
    Could not deduce (Eq b) arising from a use of `GADTBag'
    from the context (Eq a)

Это нефиксируется (со стандартным классом Functor), потому что я не могу ограничить тип fmap, но для нового списка должен быть.

версия ограничения данных

Можем ли мы сделать то, что вы спросили? Ну, да, за исключением того, что вы должны продолжать повторять ограничение Eq везде, где вы используете конструктор:

{-# LANGUAGE DatatypeContexts #-}

data Eq a => EqBag a = EqBag {unEqBag :: [a]}
  deriving Show

count' a (EqBag xs) = length.filter (==a) $ xs
size' (EqBag xs) = length xs   -- Note: doesn't use (==) at all

Отправляйтесь в ghci, чтобы узнать некоторые менее красивые вещи:

ghci> :so DataConstraints
DataConstraints_so.lhs:1:19: Warning:
    -XDatatypeContexts is deprecated: It was widely considered a misfeature, 
    and has been removed from the Haskell language.
[1 of 1] Compiling Main             ( DataConstraints_so.lhs, interpreted )
Ok, modules loaded: Main.
ghci> :t count
count :: a -> GADTBag a -> Int
ghci> :t count'
count' :: Eq a => a -> EqBag a -> Int
ghci> :t size
size :: GADTBag a -> Int
ghci> :t size'
size' :: Eq a => EqBag a -> Int
ghci> 

Таким образом, для нашей функции EqBag count требуется ограничение Eq, которое, как мне кажется, вполне разумно, но функция нашего размера также требует одного, который менее хорош. Это связано с тем, что тип конструктора EqBag равен EqBag :: Eq a => [a] -> EqBag a, и это ограничение должно быть добавлено каждый раз.

Мы также не можем создать функтор:

instance Functor EqBag where
   fmap f (EqBag xs) = EqBag (map f xs)

по той же причине, что и с GADTBag

Бесконтактные сумки

data ListBag a = ListBag {unListBag :: [a]}
  deriving Show
count'' a = length . filter (==a) . unListBag
size'' = length . unListBag

instance Functor ListBag where
   fmap f (ListBag xs) = ListBag (map f xs)

Теперь типы count '' и show '' точно так же, как мы ожидаем, и мы можем использовать стандартные классы конструкторов, такие как Functor:

ghci> :t count''
count'' :: Eq a => a -> ListBag a -> Int
ghci> :t size''
size'' :: ListBag a -> Int
ghci> fmap (Data.Char.ord) (ListBag "hello")
ListBag {unListBag = [104,101,108,108,111]}
ghci> 

Сравнение и выводы

Версия GADT автоматически распространяется на ограничение Eq везде, где используется конструктор. Средство проверки типов может полагаться на экземпляр Eq, потому что вы не можете использовать конструктор для типа, отличного от Eq.

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

Непринужденная версия хороша, потому что она не мешает вам создавать экземпляры Functor, Monad и т.д. Ограничения записываются точно, когда они нужны, не более или менее. Data.Map использует неограниченную версию частично из-за того, что unconstrained обычно считается наиболее гибким, но также отчасти потому, что он предшествует GADT с некоторым размахом, и для этого необходимо быть веской причиной потенциального нарушения существующего кода.

Как насчет вашего отличного примера User?

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

(Тем не менее, иногда у меня есть одноцелевой тип данных и в конечном итоге делает его безусловным полиморфным только потому, что я люблю использовать Functor (и аппликативный) и предпочитаю использовать fmap, чем mapBag, потому что я это чувствую яснее.)

{-# LANGUAGE GADTs #-}
import Data.String

data User s where 
   User :: (IsString s, Show s) => s -> User s

name :: User s -> s
name (User s) = s

instance Show (User s) where  -- cool, no Show context
  showsPrec i (User s) = showParen (i>9) (("User " ++ show s) ++)

instance (IsString s, Show s) => IsString (User s) where
  fromString = User . fromString

Обратите внимание, что поскольку fromString строит значение типа User a, нам нужен контекст явно. В конце концов, мы составили конструктор User :: (IsString s, Show s) => s -> User s. Конструктор User устраняет необходимость в явном контексте, когда мы сопоставляем шаблон (destruct), потому что он уже применял ограничение, когда мы использовали его как конструктор.

Нам не нужен контекст Show в экземпляре Show, потому что мы использовали (User s) в левой части в совпадении с шаблоном.

Ответ 2

Ограничения

Проблема заключается в том, что ограничения не являются свойством типа данных, а работают с ними алгоритм/функция. Для разных функций могут потребоваться разные и уникальные ограничения.

A Box пример

В качестве примера предположим, что мы хотим создать контейнер с именем Box, который содержит только 2 значения.

data Box a = Box a a

Мы хотим, чтобы:

  • быть видимым
  • позволяют сортировать два элемента через sort

Имеет ли смысл применить ограничение типа Ord и Show на тип данных? Нет, потому что сам тип данных может отображаться только или сортироваться, и поэтому ограничения связаны с его использованием, а не с определением.

instance (Show a) => Show (Box a) where
    show (Box a b) = concat ["'", show a, ", ", show b, "'"]

instance (Ord a) => Ord (Box a) where
    compare (Box a b) (Box c d) =
        let ca = compare a c
            cb = compare b d
        in if ca /= EQ then ca else cb

Случай Data.Map

Data.Map Ord Ограничения на тип действительно нужны только тогда, когда в контейнере есть > 1 элемент. В противном случае контейнер можно использовать даже без клавиши Ord. Например, этот алгоритм:

transf :: Map NonOrd Int -> Map NonOrd Int
transf x = 
    if Map.null x
        then Map.singleton NonOrdA 1
        else x

Live demo

отлично работает без ограничения Ord и всегда создает непустую карту.

Ответ 3

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

data Ord k => MapDTC k a

тогда @jefffrey transf отклоняется. Но мы должны, вероятно, иметь transf _ = return (NonOrdA, 1).

В некотором смысле контекст - это документация, в которой говорится, что "каждая Карта должна иметь упорядоченные ключи". Если вы посмотрите на все функции в Data.Map, вы получите аналогичный вывод: "каждая полезная карта имеет упорядоченные ключи". Хотя вы можете создавать карты с неупорядоченными клавишами, используя

mapKeysMonotonic :: (k1 -> k2) -> Map k1 a -> Map k2 a
singleton :: k2 a -> Map k2 a

Но как только вы попытаетесь сделать что-нибудь полезное с ними, вы получите несколько позже No instance for Ord k2.