Ответ 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)
в левой части в совпадении с шаблоном.