本文介绍了为什么数据约束是一件坏事?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I know this question has been asked and answered lots of times but I still don't really understand why putting constraints on a data type is a bad thing.

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

For example, let's take Data.Map k a. All of the useful functions involving a Map need an Ord k constraint. So there is an implicit constraint on the definition of Data.Map. Why is it better to keep it implicit instead of letting the compiler and programmers know that Data.Map needs an orderable key.

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

Also, specifying a final type in a type declaration is something common, and one can see it as a way of "super" constraining a data type.

例如,我会写

data User = User { name :: String }

,这是可以接受的.但是,这不是

受约束的版本的

and that's acceptable. However is that not a constrained version of

data User' s = User' { name :: s }

在我为 User 类型编写的全部功能中,有99%不需要 String ,少数几个可能只需要s IsString Show .

After all 99% of the functions I'll write for the User type don't need a String and the few which will would probably only need s to be IsString and Show.

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

So, why is the lax version of User considered bad:

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

同时认为 User User'都很好?

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

I'm asking this, because lots of the time, I feel I'm unnecessarily narrowing my data (or even function) definitions, just to not have to propagate constraints.

据我了解,数据类型约束仅适用于构造函数,不会传播.所以我的问题是,为什么数据类型约束不能按预期工作(并传播)?无论如何,这是一个扩展,所以如果社区认为有用,为什么没有一个新的扩展来正确地处理 data ?

As far as I understand, data type constraints only apply to the constructor and don't propagate. So my question is then, why do data type constraints not work as expected (and propagate)? It's an extension anyway, so why not have a new extension doing data properly, if it was considered useful by the community?

推荐答案

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

让我们以一个袋子的案例研究为例,我们所关心的只是袋子中有多少次.(就像无序序列一样.我们几乎总是需要一个Eq约束来对其进行任何有用的操作.

Let's use a case study of a Bag where all we care about is how many times something is in it. (Like an unordered sequence. We nearly always need an Eq constraint to do anything useful with it.

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

I'll use the inefficient list implementation so as not to muddy the waters over the Data.Map issue.

要做的简单的方法是使用GADT:

The easy way to do what you're after is to use a GADT:

以下说明 Eq 约束如何不仅迫使您在制作GADTBags时将类型与Eq实例一起使用,而且还会在 GADTBag 构造函数出现的任何位置隐式地提供该实例.这就是为什么 count 不需要 Eq 上下文,而 countV2 则需要-它不使用构造函数的原因:

Notice below how the Eq constraint not only forces you to use types with an Eq instance when making GADTBags, it provides that instance implicitly wherever the GADTBag constructor appears. That's why count doesn't need an Eq context, whereas countV2 does - it doesn't use the constructor:

{-# 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 .)

Now we didn't need the Eq constraint when we found the total size of the bag, but it didn't clutter up our definition anyway. (We could have used size = length . unGADTBag just as well.)

现在让我们做一个函子:

Now lets make a functor:

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 的类型,但是需要新列表.

That's unfixable (with the standard Functor class) because I can't restrict the type of fmap, but need to for the new list.

我们可以按照您的要求做吗?好吧,是的,除了在使用构造函数的任何地方都必须重复重复Eq约束:

Can we do as you asked? Well, yes, except that you have to keep repeating the Eq constraint wherever you use the constructor:

{-# 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找出一些不太漂亮的东西:

Let's go to ghci to find out some less pretty things:

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 ,并且每次都必须添加此约束.

So our EqBag count' function requires an Eq constraint, which I think is perfectly reasonable, but our size' function also requires one, which is less pretty. This is because the type of the EqBag constructor is EqBag :: Eq a => [a] -> EqBag a, and this constraint must be added every time.

我们在这里也不能做函子:

We can't make a functor here either:

instance Functor EqBag where
   fmap f (EqBag xs) = EqBag (map f xs)

出于与GADTBag完全相同的原因

for exactly the same reason as with the 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这样的标准构造函数类:

Now the types of count'' and show'' are exactly as we expect, and we can use standard constructor classes like 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类型.

Comparison and conclusions

The GADTs version automagically propogates the Eq constraint everywhere the constructor is used. The type checker can rely on there being an Eq instance, because you can't use the constructor for a non-Eq type.

DatatypeContexts版本强制程序员手动推广Eq约束,如果您需要,对我来说这很好,但是已弃用,因为它没有给您带来比GADT多的东西,并且被许多人认为是毫无意义的和烦人.

The DatatypeContexts version forces the programmer to manually propogate the Eq constraint, which is fine by me if you want it, but is deprecated because it doesn't give you anything more than the GADT one does and was seen by many as pointless and annoying.

不受限制的版本很好,因为它不会阻止您创建Functor,Monad等实例.约束是在需要时准确地写的,或多或少.Data.Map使用不受约束的版本,部分是因为通常认为不受约束是最灵活的,但是部分是因为它比GADT早了一些,并且有必要提出令人信服的理由来破坏现有代码.

The unconstrained version is good because it doesn't prevent you from making Functor, Monad etc instances. The constraints are written exactly when they're needed, no more or less. Data.Map uses the unconstrained version partly because unconstrained is generally seen as most flexible, but also partly because it predates GADTs by some margin, and there needs to be a compelling reason to potentially break existing code.

我认为这是受益于类型限制的单用途数据类型的一个很好的例子,我建议您使用GADT来实现它.

I think that's a great example of a one-purpose data type that benefits from a constraint on the type, and I'd advise you to use a GADT to implement it.

(也就是说,有时我只有一种数据类型,最终因为我喜欢使用Functor(和Applicative)而使其不受限制地成为多态的,所以宁愿使用 fmap 而不是 mapBag ,因为我觉得它更清楚.)

(That said, sometimes I have a one-purpose data type and end up making it unconstrainedly polymorphic just because I love to use Functor (and Applicative), and would rather use fmap than mapBag because I feel it's clearer.)

{-# 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 构造函数在我们进行模式匹配(销毁)时消除了对显式上下文的需要,因为在将其用作构造函数时,它已经强制执行了约束.

Notice since fromString does construct a value of type User a, we need the context explicitly. After all, we composed with the constructor User :: (IsString s, Show s) => s -> User s. The User constructor removes the need for an explicit context when we pattern match (destruct), becuase it already enforced the constraint when we used it as a constructor.

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

We didn't need the Show context in the Show instance because we used (User s) on the left hand side in a pattern match.

这篇关于为什么数据约束是一件坏事?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-04 20:56