我知道这个问题已被多次询问和回答,但我仍然不明白为什么对数据类型设置约束是件坏事。
例如,让我们Data.Map k a
。涉及Map
的所有有用函数都需要Ord k
约束。因此Data.Map
的定义存在隐式约束。为什么更好地保持隐含而不是让编译器和程序员知道Data.Map
需要可订购密钥。
此外,在类型声明中指定最终类型是常见的,并且可以将其视为" super"约束数据类型。
例如,我可以写
data User = User { name :: String }
那是可以接受的。但是,这不是约束版本的
data User' s = User' { name :: s }
在为User
类型编写的所有99%的功能之后,不需要String
以及可能只需要s
的少数功能为IsString
和Show
。
那么,为什么User
的松散版本被认为是错误的:
data (IsString s, Show s, ...) => User'' { name :: s }
虽然User
和User'
都被认为是好的吗?
我问这个,因为很多时候,我觉得我不必要地缩小我的数据(甚至函数)定义,只是为了不必传播约束。
据我所知,数据类型约束仅适用于构造函数并且不会传播。那么我的问题是,为什么数据类型约束不能按预期工作(和传播)?无论如何它都是一个扩展,那么为什么没有一个新的扩展正确地执行data
,如果它被社区认为是有用的呢?
答案 0 :(得分:15)
TL; DR:
使用GADT提供隐式数据上下文
如果可以使用Functor实例等,请不要使用任何类型的数据约束。
无论如何,地图太旧了,无法改为GADT。
如果您想查看使用GADT的User
实施
让我们使用一个Bag的案例研究,我们关心的是它中有多少次。 (就像一个无序的序列。我们几乎总是需要一个Eq约束来做任何有用的事情。
我将使用低效的列表实现,以免混淆Data.Map问题。
做你想做的事情的简单方法是使用GADT:
请注意Eq
约束如何强制您在制作GADTBags时使用带有Eq实例的类型,它会在GADTBag
构造函数出现的任何位置隐式提供该实例。这就是为什么count
不需要Eq
上下文,而countV2
呢?它不使用构造函数:
{-# 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计数'函数需要一个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>
GADTs版本在使用构造函数的每个地方自动传播Eq约束。类型检查器可以依赖于Eq实例,因为您不能将构造函数用于非Eq类型。
DatatypeContexts版本强制程序员手动传播Eq约束,如果你需要它,这对我来说很好,但是因为它不会给你任何东西而不是GADT,并且很多人都认为它是毫无意义的而烦人。
无约束版本很好,因为它不会阻止你制作Functor,Monad等实例。约束是在需要时准确写入的,不多或少。 Data.Map使用无约束版本,部分原因是因为无约束通常被认为是最灵活的,但也部分是因为它比GADT早一些边缘,并且需要有一个令人信服的理由可能破坏现有代码。
User
示例怎么样?我认为这是一个单一目的数据类型的一个很好的例子,它受益于对类型的约束,我建议你使用GADT来实现它。
(也就是说,有时候我有一个单一用途的数据类型,最终只是因为我喜欢使用Functor(和Applicative),而不是使用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
编写。模式匹配(destruct)时,User
构造函数不需要显式上下文,因为当我们将它用作构造函数时,它已经强制执行了约束。
我们在Show实例中不需要Show上下文,因为我们在模式匹配的左侧使用了(User s)
。
答案 1 :(得分:11)
问题在于约束不是数据类型的属性,而是对它们进行操作的算法/函数的属性。不同的功能可能需要不同的和唯一的约束。
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
Ord
类型的约束。容器中的1个元素。否则,即使没有transf :: Map NonOrd Int -> Map NonOrd Int
transf x =
if Map.null x
then Map.singleton NonOrdA 1
else x
密钥,容器也可以使用。例如,这个算法:
Ord
在没有{{1}}约束的情况下工作正常,并且始终生成非空映射。
答案 2 :(得分:1)
使用DataTypeContexts
会减少您可以编写的程序数量。如果大多数非法程序都是无意义的,您可能会说与ghc传入未使用的类型类字典相关的运行时成本是值得的。例如,如果我们有
data Ord k => MapDTC k a
然后@ jefffrey的transf
被拒绝。但我们应该改为transf _ = return (NonOrdA, 1)
。
在某种意义上,上下文是指“每个Map必须具有有序键”的文档。如果你查看Data.Map中的所有函数,你会得到一个类似的结论“每个有用的Map都有订单键”。虽然您可以使用
创建带有无序键的地图mapKeysMonotonic :: (k1 -> k2) -> Map k1 a -> Map k2 a
singleton :: k2 a -> Map k2 a
但是当你尝试对他们做任何有用的事情的那一刻,你会稍后结束No instance for Ord k2
。