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

mb1*_*b14 20 haskell

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

例如,我们来看看Data.Map k a.所有涉及Map需要Ord k约束的有用函数.所以对于定义有一个隐含的约束Data.Map.为什么更好地保持隐含而不是让编译器和程序员知道Data.Map需要可订购密钥.

此外,在类型声明中指定最终类型是常见的,可以将其视为"超级"约束数据类型的方式.

例如,我可以写

data User = User { name :: String }
Run Code Online (Sandbox Code Playgroud)

这是可以接受的.然而,这不是一个受限制的版本

data User' s = User' { name :: s }
Run Code Online (Sandbox Code Playgroud)

我会为写功能的所有99%后User类型并不需要String和这将很可能只需要少数sIsStringShow.

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

data (IsString s, Show s, ...) => User'' { name :: s }
Run Code Online (Sandbox Code Playgroud)

而这两个UserUser'被认为是好?

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

更新

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

And*_*ewC 16

TL; DR:
使用GADT提供隐式数据上下文.
如果你可以使用Functor实例等,请不要使用任何类型的数据约束
.无论如何,Map太旧而无法更改为GADT.如果要查看User使用GADT 的实现,请滚动到底部


让我们使用一个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
Run Code Online (Sandbox Code Playgroud)
ghci> count 'l' (GADTBag "Hello")
2
ghci> :t countV2
countV2 :: Eq a => a -> GADTBag a -> Int
Run Code Online (Sandbox Code Playgroud)

现在,当我们找到袋子的总尺寸时,我们不需要Eq约束,但无论如何它都没有弄乱我们的定义.(我们也可以使用size = length . unGADTBag.)

现在让我们做一个仿函数:

instance Functor GADTBag where
  fmap f (GADTBag xs) = GADTBag (map f xs)
Run Code Online (Sandbox Code Playgroud)

哎呀!

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

这是不可修复的(使用标准的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
Run Code Online (Sandbox Code Playgroud)

让我们去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> 
Run Code Online (Sandbox Code Playgroud)

所以我们的EqBag计数'函数需要一个Eq约束,我认为这是完全合理的,但我们的尺寸'函数也需要一个,这不太漂亮.这是因为EqBag构造函数的类型是EqBag :: Eq a => [a] -> EqBag a,并且每次都必须添加此约束.

我们不能在这里制作一个仿函数:

instance Functor EqBag where
   fmap f (EqBag xs) = EqBag (map f xs)
Run Code Online (Sandbox Code Playgroud)

与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)
Run Code Online (Sandbox Code Playgroud)

现在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> 
Run Code Online (Sandbox Code Playgroud)

比较和结论

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
Run Code Online (Sandbox Code Playgroud)

通知自fromString 的确构造类型的值User a,我们需要明确的背景下.毕竟,我们用构造函数编写User :: (IsString s, Show s) => s -> User s.该User构造消除了,当我们模式匹配(毁灭),原因是其已实施的约束时,我们用它作为构造一个明确的情况下的需要.

我们在Show实例中不需要Show上下文,因为我们(User s)在模式匹配的左侧使用了它.

  • 你有没有尝试过像GADT这样的东西来编写类似`Data.Map`的东西?好吧,你没有摆脱任何创建`Map`的函数的实例要求,而没有得到一个作为输入.这使得像`singleton`这样的东西严格缺乏用处,因为它现在没有'Ord k`约束.而且大多数情况下,你只是让文档撒谎.它不像那些函数在没有`Ord`实例的类型上工作.如果您的文档在类型中提到"Ord"而不是假装它不是,那么对于您的库的用户来说更好. (2认同)
  • @Carl使用GADT会将Ord上下文切换为仅在提供新键时才会出现,所以是的,`singleton`(和`insert`)会有一个Ord上下文,但是在一个键类型上使用singleton会阻止你使用其他键无论如何,地图功能都是无用的.`singleton id 7`?同样`fromList`和`fromSet`会获得Ord上下文(`mapKeys`无论如何都有新的键类型),但是像`union`,`intersection`,`difference`这样的组合函数会丢失它们.这不是说谎,而是减少了位置.当您使用地图而不是使用地图时,文档会将Ord视为上下文. (2认同)

Sho*_*hoe 11

约束

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

一个Box例子

举个例子,我们假设我们要创建一个Box只包含2个值的容器.

data Box a = Box a a
Run Code Online (Sandbox Code Playgroud)

我们希望它:

  • 显得可见
  • 允许通过分类两个元素 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
Run Code Online (Sandbox Code Playgroud)

这个Data.Map案子

Data.MapOrd只有当容器中有> 1个元素时,才真正需要对类型的约束.否则,即使没有Ord钥匙,容器也是可用的.例如,这个算法:

transf :: Map NonOrd Int -> Map NonOrd Int
transf x = 
    if Map.null x
        then Map.singleton NonOrdA 1
        else x
Run Code Online (Sandbox Code Playgroud)

Live demo

在没有Ord约束的情况下工作正常,并且始终生成非空映射.

  • `Map`只有*才能拥有'Eq`键.让它们成为'Ord`是一个*效率*问题. (4认同)
  • 好的,但是你真的不需要一个`Map`,如果你只有一个元素,不是吗? (2认同)