Haskell 为什么数据约束是件坏事?

Haskell 为什么数据约束是件坏事?,haskell,Haskell,我知道这个问题已经被问了很多次,也被回答了很多次,但我仍然不明白为什么对数据类型施加约束是件坏事 例如,让我们以Data.mapka为例。所有涉及Map的有用函数都需要Ord k约束。因此,Data.Map的定义有一个隐含的约束。为什么最好保持它的隐式,而不是让编译器和程序员知道Data.Map需要一个可排序的键 此外,在类型声明中指定最终类型是常见的,可以将其视为“超级”约束数据类型的一种方式 例如,我会写作 data User = User { name :: String } 这是可以接

我知道这个问题已经被问了很多次,也被回答了很多次,但我仍然不明白为什么对数据类型施加约束是件坏事

例如,让我们以
Data.mapka
为例。所有涉及
Map
的有用函数都需要
Ord k
约束。因此,
Data.Map
的定义有一个隐含的约束。为什么最好保持它的隐式,而不是让编译器和程序员知道
Data.Map
需要一个可排序的键

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

例如,我会写作

data User = User { name :: String }
这是可以接受的。然而,这不是一个受约束的版本吗

data User' s = User' { name :: s }
毕竟,我将为
用户
类型编写的99%的函数不需要
字符串
,少数函数可能只需要
s
成为
IsString
Show

那么,为什么松弛版的
User
被认为是不好的:

data (IsString s, Show s, ...) => User'' { name :: s }
User
User'
都被认为是好的

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

更新 据我所知,数据类型约束只适用于构造函数,不会传播。所以我的问题是,为什么数据类型约束不能按预期工作(并传播)?无论如何,它是一个扩展,如果社区认为它有用,为什么不让一个新的扩展正确地处理
数据呢?

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

示例 例如,假设我们要创建一个名为
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
案例 只有当容器中有>1个元素时,才真正需要对类型的
Data.Map
约束。否则,即使没有
Ord
键,容器也可用。例如,此算法:

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

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

TL;博士:
使用GADT提供隐式数据上下文。
如果可以使用函子实例等,请不要使用任何类型的数据约束。
地图太旧了,无论如何都不能换成游荡的。 如果要查看使用GADT的
用户
实现,请滚动至底部


让我们以一个袋子为例,我们只关心袋子里的东西有多少次。(就像一个无序的序列。我们几乎总是需要一个Eq约束来做任何有用的事情

我将使用低效的列表实现,以免在数据映射问题上弄乱局面

GADTs-数据约束“问题”的解决方案 要完成您的目标,最简单的方法是使用GADT:

请注意下面的
Eq
约束如何在生成GADTBag时不仅强制您将类型与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
{-# 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
现在,当我们找到行李的总尺寸时,我们不需要Eq约束,但它不会扰乱我们的定义(我们也可以使用
size=length.unGADTBag

现在让我们制作一个函子:

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

这是不可调整的(对于标准的Functor类),因为我不能限制
fmap
的类型,但需要为新列表设置限制

数据约束版本 我们能按你的要求做吗?好的,是的,除了你必须在使用构造函数的地方不断重复Eq约束:

{-# 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
{-# 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约束,我认为这是完全合理的,但是我们的size'函数也需要一个Eq约束,它不太漂亮。这是因为
EqBag
构造函数的类型是
EqBag::Eq a=>[a]->EqBag a
,每次都必须添加这个约束

我们也不能在这里生成函子:

instance Functor EqBag where
   fmap f (EqBag xs) = EqBag (map f xs)
原因与GADTBag完全相同

无约束袋 现在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早一段时间d需要有一个令人信服的理由来进行潜在的bre
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> 
{-# 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
data Ord k => MapDTC k a
mapKeysMonotonic :: (k1 -> k2) -> Map k1 a -> Map k2 a
singleton :: k2 a -> Map k2 a