商数类型如何帮助安全地暴露模块内部?

时间:2014-05-11 18:15:48

标签: haskell functional-programming type-systems type-theory

阅读关于商类型及其在函数式编程中的使用,我遇到了this post。作者提到Data.Set作为一个模块的例子,它提供了大量需要访问模块内部的函数:

  

Data.Set有36个函数,当真正需要确保集合含义(“这些元素不同”)的所有函数都是toListfromList时。

作者的观点似乎是,如果我们忘记了一些只能使用模块内部有效实现的功能,我们需要“打开模块并打破抽象”。

然后他说

  

我们可以用商型来缓解所有这些混乱。

但没有解释该声明。

所以我的问题是:商数类型如何在这里起作用?


修改

我做了一些研究,发现了一篇论文"Constructing Polymorphic Programs with Quotient Types"。它详细阐述了容器的容器,并在摘要和引言中提到了“有效”这个词。但如果我没有误读,它就没有给出任何有效表示“隐藏”商容器的例子。


编辑2

在第3章的"[PDF] Programming in Homotopy Type Theory" paper中揭示了更多内容。使用了商类型可以作为从属和实现的事实。引入了关于抽象类型的视图(看起来与我的类型类非常相似),并提供了一些相关的Agda代码。然而本章重点关注关于抽象类型的推理,所以我不确定这与我的问题有什么关系。

3 个答案:

答案 0 :(得分:9)

我最近做了一个blog post about quotient types,我在这里发表评论。除了问题中引用的论文之外,博客文章还可以提供一些额外的背景信息。

答案实际上非常简单。达到目的的一种方法是提出问题:为什么我们首先使用抽象数据类型Data.Set

有两个截然不同且可分离的原因。第一个原因是隐藏接口背后的内部类型,以便我们将来可以替换一个全新的类型。第二个原因是对内部类型的值强制隐式不变量。商数类型及其双子集类型允许我们使不变量显式并由类型检查器强制执行,以便我们不再需要隐藏表示。所以让我非常清楚:商(<子>) 提供隐藏的任何实现。如果使用列表作为表示来实现带有商类型的Data.Set,那么稍后您决定要使用树,则需要更改使用您的类型的所有代码。

让我们从一个更简单的例子开始(左下角)。 Haskell有Integer类型但不是Natural类型。使用组合语法将Natural指定为子集类型的简单方法是:

type Natural = { n :: Integer | n >= 0 }

我们可以使用智能构造函数将其实现为抽象类型,该构造函数在给定负Integer时会引发错误。此类型表示只有类型Integer的值的子集有效。我们可以用来实现这种类型的另一种方法是使用商类型:

type Natural = Integer / ~ where n ~ m = abs n == abs m

某种类型h :: X -> T的任何函数T都会在等价关系X的{​​{1}}上引出商数类型。此表单的商数类型更容易编码为抽象数据类型。但是,一般而言,可能没有这样方便的功能,例如:

x ~ y = h x == h y

(关于商类型如何与setoids相关,商类型是强制你强制尊重其等价关系的setoid。)type Pair a = (a, a) / ~ where (a, b) ~ (x, y) = a == x && b == y || a == y && b == x 的第二个定义具有两个表示{{{1}的值的属性。 1}},比方说。即,Natural2。商类型方面表示我们被允许对底层2做任何我们想做的事情,只要我们永远不会产生区分这两个代表的结果。另一种看待这种情况的方法是我们可以使用子类型编码商类型:

-2

不幸的是,Integer等于检查函数的相等性。

缩小,子集类型为值生成器添加约束,商类型为值的使用者添加约束。由抽象数据类型强制执行的不变量可以是这些的混合。实际上,我们可能决定将X/~ = forall a. { f :: X -> a | forEvery (\(x, y) -> x ~ y ==> f x == f y) } -> a 表示为以下内容:

forEvery

请注意,这一点并不需要我们实际执行 Setdata Tree a = Empty | Branch (Tree a) a (Tree a) type BST a = { t :: Tree a | isSorted (toList t) } type Set a = { t :: BST a | noDuplicates (toList t) } / ~ where s ~ t = toList s == toList t isSorted。我们只是&#34;需要说服类型检查器,这种类型的函数的实现将满足这些谓词。商类型允许我们具有冗余表示,同时强制我们以相同的方式处理等效表示。这并不意味着我们不能利用我们所拥有的特定表示来产生一个值,这只意味着我们必须说服类型检查器,在给定不同的等效表示的情况下,我们将产生相同的值。例如:

noDuplicates

对此的证明义务是具有相同元素的任何二叉搜索树中最右边的元素是相同的。正式地,只要toList,它就是maximum :: Set a -> a maximum s = exposing s as t in go t where go Empty = error "maximum of empty Set" go (Branch _ x Empty) = x go (Branch _ _ r) = go r 。如果我们使用表示保证树将是平衡的,例如AVL树,此操作将go t == go t'转换为列表,并从列表中选择最大值toList t == toList t'。即使使用此表示,此代码也比转换为列表并从列表中获取最大值更有效。注意,我们无法实现显示O(log N)的树结构的函数。这样的功能会是错误的。

答案 1 :(得分:5)

我会给出一个比较清楚的简单例子。不可否认,我自己并没有真正看到这会如何有效地转化为Set

data Nat = Nat (Integer / abs)

为了安全地使用它,我们必须确保任何函数Nat -> T(为了简单起见,使用一些非商数T)并不依赖于实际的整数值,而只取决于绝对。要做到这一点,没有必要完全隐藏Integer;阻止你直接匹配就足够了。相反,编译器可能会重写匹配项,例如

even' :: Nat -> Bool
even' (Nat 0) = True
even' (Nat 1) = False
even' (Nat n) = even' . Nat $ n - 2

可以改写为

even' (Nat n') = case abs n' of
           [|abs 0|]  -> True
           [|abs 1|]  -> False
           n          -> even' . Nat $ n - 2

这种重写会指出等同违规,例如

bad (Nat 1) = "foo"
bad (Nat (-1)) = "bar"
bad _ = undefined

将重写为

bad (Nat n') = case n' of
      1 -> "foo"
      1 -> "bar"
      _ -> undefined

这显然是重叠的模式。

答案 2 :(得分:2)

免责声明:我在读完这个问题后就读了一下商品类型。

我认为作者只是说集合可以被描述为列表上的商类型。即:(制作一些像哈克尔一样的语法):

data Set a = Set [a] / (sort . nub) deriving (Eq)

Set a只是一个[a],其中两个Set a之间的相等性取决于基础列表的sort . nub是否相等。< / p>

我可以这样明确地做到这一点,我想:

import Data.List

data Set a = Set [a] deriving (Show)

instance (Ord a, Eq a) => Eq (Set a) where
  (Set xs) == (Set ys) = (sort $ nub xs) == (sort $ nub ys)

不确定这是否实际上是作者的意图,因为这不是实现集合的特别有效的方式。有人可以随意纠正我。