在Haskell中将类型变量限制为数据声明中的类类型

sdnqo3pr  于 2023-06-23  发布在  其他
关注(0)|答案(2)|浏览(160)

我想声明一个数据类型,它是由一系列可比较的元素构成的。这是因为我写了一个函数,只有当它的输入列表被排序时才能正常工作,我想找到一种方法来让编译器阻止它被意外地用于未排序的列表。(我不认为有任何方法可以阻止用户撒谎,但我至少希望他们将列表声明为已排序)。
除非列表的元素是Ord,否则(对我来说)有一个排序的列表是没有意义的,我想让编译器至少强制执行这么多。

data WrappedList a = WrappedList [a]

data SortedList (Ord a) => a = SortedList [a]

SortedList是我失败的尝试之一,而WrappedList实际上可以编译。我找不到这方面的任何例子,所以也许我完全错过了重点。
(N.B.我刚刚开始学习Haskell,这是一个玩具问题。

von4xj4u

von4xj4u1#

假设你可以创建SortedList类型,你想写的函数很可能有一个看起来有点像这样的类型:

myFunc :: Ord a => SortedList a -> ...

如果myFunc没有Ord约束,那么它将无法使用比较函数(这意味着它完全无法观察列表是否排序;这并不意味着它已经排序 * 完全 * 无用,但你肯定会失去很大一部分知道它已经排序的效用)。
这意味着,如果有人想调用myFunc的类型 * doesn 't * 有一个Ord约束,编译器将调用他们无论如何。因此,如果他们不能证明Ord a,阻止他们构建SortedList a实际上不会捕获任何额外的错误。因此,简单地将Ord a约束添加到SortedList本身可能不是您应该投入大量精力的事情;它实际上买不到什么东西。像这样的简单类型:

data SortedList a = SortedList [a]

-- Actually it could be a newtype, which is a bit more efficient; but don't worry
-- about it if you don't know what that is yet
newtype SortedList a = SortedList [a]

可以很好地创建这样一种情况,即很难意外地调用未排序列表上的函数(调用者必须故意对你撒谎,或者至少疏忽地假设来自其他地方的列表已经排序),而Ord对函数的约束,如果函数按照SortedList中的顺序执行任何有趣的操作,则会捕获任何使用您的函数的列表的人,这些列表实际上不能执行。不能被排序(按规范顺序),因为它们的元素类型甚至没有这样的顺序。
Haskell实际上 * 曾经 * 有a feature for doing exactly what you're after,但在拥有它的经验之后,Maven和社区的共识是最好不要有这个功能。它已被弃用,在较新的GHC版本中默认不再可用,并将在某个时候完全删除。因此,既然你正在学习Haskell,我建议你永远不要学习如何使用这个功能。学会不用它来解决问题是一项有用的技能,它将延续到未来的Haskell中;学着依赖它是死路一条
如果你真的想在构建SortedList Package 器的时候进行Ord a检查,在更现代的Haskell中,这样做的方法是使用GADTs扩展。然而,这是一个更高级的Haskell特性,所以当你还在学习的时候,可能不应该尝试解决这个问题。
但是为了完整性,它允许你写一个像这样的类型:

data SortedList a where
  SortedList :: Ord a => [a] -> SortedList a

这意味着当应用SortedList构造函数时,编译器不仅会检查Ord a,它实际上会将Ord示例存储在SortedList值中。使用SortedList的函数实际上不需要Ord约束,因为它们在SortedList上进行模式匹配时可以访问存储的示例。
但是,在任何地方使用这种技术时都要小心;它会导致显著的性能问题。如果你在一个存储的示例中使用了很多值,那么你可能会使用大量的内存来存储对同一个示例的引用(以及时间对所有这些引用的解引用)。但更糟糕的是,这种技术使编译器通常可以进行的许多优化失败。编译器通常可以内联和专门化具有类型类约束的函数,以便它们最终调用静态已知的类型类方法,这比通过运行时字典调用它们要快得多。当 * 你 * 管理字典传递(通过将它们存储在GADT中)而不是编译器时,编译器进行这些优化会变得更加困难。
如果您进一步深入研究GADT,您还会发现它们允许您“隐藏”类型参数,并且 * 这 * 打开了一个巨大的蠕虫,您需要非常牢固地掌握Haskell的类型系统和语法才能掌握。或者至少当出现错误时您可能会得到的错误消息对新手来说非常令人困惑,这使得它们很难纠正。
GADT是一个非常强大的特性,它支持构建代码的方法,而这些方法是普通Haskell数据类型无法实现的。我个人的原则是在这种情况下保存它们,而不是仅仅使用它们来捕获编译器无论如何都会捕获的错误。这是一个成本效益的权衡,你必须为自己。但是,在您的Haskell之旅中,GADT确实值得您学习和掌握!
如果你想更进一步,* 实际上 * 有一个编译器强制的数据类型是一个排序列表,那么实际上有很多方法可以做到这一点。最直接的方法甚至不需要GADTs!诀窍是使用模块系统。(如果你还不习惯编写多模块程序,那么可以把这个留到以后;你让编译器强制执行你的不变量的本能是非常好的Haskell,但你不可能在你刚刚开始的时候就做到Haskell所能做到的一切)
在一个与其他代码分开的模块中(即在文件SortedList.hs)中,编写如下内容:

module SortedList
  ( SortedList   -- note this just exports the *type* name, not the constructor
  , fromUnsorted
  , empty
  , elements
  , sortedInsert
  , unsafeFromSorted
  )
where

import Data.List (sort, insert)

newtype SortedList a = SL [a]

fromUnsorted :: Ord a => [a] -> SortedList a
fromUnsorted = SL . sort

empty :: SortedList a
empty = SL []

elements :: SortedList a -> [a]
elements (SL xs) = xs

sortedInsert :: Ord a => a -> SortedList a -> SortedList a
sortedInsert x (SL xs) = SL $ insert x xs

-- Optionally include a function like this to allow a programmer to declare
-- that a list is *already* sorted. Having an option like this is equivalent
-- to exporting the constructor, so it loses the guarantee that the list is
-- *always* sorted. But there are often ways to build a list that is sorted by
-- construction (e.g. [1..100]), so having to call `sort` on them is a
-- performance hit. Take your pick between performance and safety.
unsafeFromSorted :: [a] -> SortedList a
unsafeFromSorted = SL

关键是我们没有导出构造函数SL(我将其命名为与SortedList不同,只是为了避免在谈论它们时产生混淆)。本模块之外的任何人都不能将SL应用于随机未排序列表。他们必须使用fromUnsorted函数来对列表进行排序。或者,如果他们已经有一个SortedList,他们可以使用sortedInsert来添加一个新元素,但这保持了输出列表是排序的属性,如果输入是。(或者他们可以使用empty,因为空列表显然总是排序的)
如果这些是程序的其他部分访问SortedList的唯一方式,那么您总是知道这样的列表是排序的。elements函数将元素提取为原始列表(由于构造函数不可用,因此没有人可以通过模式匹配来获取它们,因此需要原始列表);这放弃了保证,但允许对普通列表函数的完全访问。
sortedInsert函数并不是绝对必要的;你可以使用fromUnsorted . insert x . elements来做同样的事情。但这效率要低得多,因为它需要重新检查列表是否已排序,以便在最后将其转换回SortedList,并且我们知道,如果输入被排序,insert总是会产生排序结果。因此,包含insert的模拟可以更轻松、更高效地使用SortedList s。类似地,你可以考虑其他列表操作,这些操作可以(更有效地)以一种维护排序不变式的方式完成,并包括那些操作。你做的越多,就越不需要像unsafeFromSorted这样的东西,因为人们将能够直接使用SortedList,而不是使用普通的列表,然后在最后转换它们。

gmxoilav

gmxoilav2#

您可以使用DataTypeContexts扩展名声明它,但如文档中所述,它被认为是一个错误功能,不鼓励您使用它。
你可以在DatatypeContexts Deprecated in Latest GHC: Why?找到为什么它被认为是一个错误的特性。

data Ord a => SortedList = SortedList [a]

您可以向实际需要约束的函数添加约束,而不是向数据类型添加约束。例如,您可以将Ord a =>添加到一个函数中以构建SortedList,或者添加到一个函数中以使用二进制搜索查找元素。

buildSortedList :: Ord a => [a] -> SortedList [a]
buildSortedList = ...

相关问题