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