为什么限制数据是件坏事?

时间:2014-06-28 09:49:44

标签: haskell

我知道这个问题已被多次询问和回答,但我仍然不明白为什么对数据类型设置约束是件坏事。

例如,让我们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的少数功能为IsStringShow

那么,为什么User的松散版本被认为是错误的:

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

虽然UserUser'都被认为是好的吗?

我问这个,因为很多时候,我觉得我不必要地缩小我的数据(甚至函数)定义,只是为了不必传播约束。

更新

据我所知,数据类型约束仅适用于构造函数并且不会传播。那么我的问题是,为什么数据类型约束不能按预期工作(和传播)?无论如何它都是一个扩展,那么为什么没有一个新的扩展正确地执行data,如果它被社区认为是有用的呢?

3 个答案:

答案 0 :(得分:15)

TL; DR:
使用GADT提供隐式数据上下文 如果可以使用Functor实例等,请不要使用任何类型的数据约束。
无论如何,地图太旧了,无法改为GADT。 如果您想查看使用GADT的User实施

,请滚动到底部

让我们使用一个Bag的案例研究,我们关心的是它中有多少次。 (就像一个无序的序列。我们几乎总是需要一个Eq约束来做任何有用的事情。

我将使用低效的列表实现,以免混淆Data.Map问题。

GADTs - 数据约束“问题”的解决方案

做你想做的事情的简单方法是使用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)

约束

问题在于约束不是数据类型的属性,而是对它们进行操作的算法/函数的属性。不同的功能可能需要不同的和唯一的约束。

A Box示例

作为一个例子,我们假设我们要创建一个名为Box的容器,它只包含2个值。

data Box a = Box a a

我们想要:

  • 可以展示
  • 允许通过sort
  • 对这两个元素进行排序

对数据类型应用OrdShow的约束是否有意义?不,因为数据类型本身只能显示或仅排序,因此约束与其使用有关,而不是它的定义。

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

Live demo

在没有{{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