有哪些程序很容易被指定为依赖类型,但实现起来很复杂?

时间:2016-11-25 21:57:25

标签: haskell functional-programming dependent-type idris

去年我问过" Dependent types can prove your code is correct up to a specification. But how do you prove the specification is correct?"。投票最多的答案提出了以下推理:

  

希望你的规范很简单,小到足以通过考试来判断,而你的实现可能要大得多。

这种推理对我来说很有意义。 Idris是测试这些概念最容易理解的语言;然而,由于它几乎可以像Haskell一样使用,它常常让程序员在旧概念中游荡,而不知道在哪里应用依赖类型。一些现实世界的例子可以对此有所帮助,因此,什么是好的,在实践中发生的程序的具体例子,很容易表达为类型,但实现起来很复杂?

2 个答案:

答案 0 :(得分:4)

我会回答这个问题:

  

通常会让程序员在旧概念中游荡,而不知道在哪里应用依赖类型

确实类型可以用来消除某些类型的愚蠢错误,比如当你以错误的顺序将一个函数应用于它的参数时,但这不是真正适用的类型。类型构造您的推理并允许放大您的计算结构。

假设您处理列表并使用headtail,但这些是部分功能,因此您决定切换到更安全的内容,现在处理NonEmpty a而不是{{ 1}}。然后你意识到你也做了很多[a] s(再次使用部分功能),并且在这种特殊情况下静态保持列表长度不会太麻烦,所以你切换到某种东西例如lookup,其中NonEmptyVec n a是向量的静态已知长度。现在你已经消除了很多可能的错误,但这是发生的最重要的事情。

最重要的是,现在你看一下类型签名,看看它们期望什么样的输入函数以及它们产生什么样的输出。函数的可能行为已经通过其类型签名缩小,现在更容易识别函数所属的管道中的位置。但是你也有更详细的类型,你的实体封装得越多:类型n的函数不再依赖于将非空列表传递给它的假设,而是明确要求这个不变量保持。你已经将类似果冻的紧密耦合计算变成了细粒度计算。

丰富的类型是指导人类(在代码编写之前,在代码编写之前,在代码编写之后)并且首先降低他们产生错误的能力 - 而不是用于后验发现它们。我认为不可避免的简单类型,因为即使你用动态类型语言编写代码,你仍然可以区分字符串和图片。

足够的聊天,这是一个有用的现实世界的例子,更重要的是,依赖类型的自然性。我在NonEmpty a -> b库的帮助下定位了一个API(这是一段很棒的代码,也是依赖类型的,所以你可能想检查它):

Servant

因此我们发送类型type API a r = ReqBody '[JSON] (Operation a) :> Post '[JSON] (Result r) 的请求(由Servant自动编码为JSON)并接收Operation a响应(由Servant自动从JSON解码)。 Result rOperation的定义如下:

Result

任务是执行操作并从服务器接收响应。但问题是,当我们data Method = Add | Get data Operation a = Operation { method :: !Method , params :: !a } data Result a = Result { result :: !a } 时,服务器会回复Add,而当我们AddResults时,服务器的响应取决于我们与Get方法。所以我们写一个类型族:

Get

代码读起来比我上面的描述更好。它只是将type family ResultOf m a where ResultOf Add a = AddResults ResultOf Get DictionaryNames = Dictionaries 提升到类型级别,因此我们定义了一个合适的单例(这是在Haskell中模拟依赖类型的方法):

Method

这是主函数的类型签名(省略了许多不相关的东西):

data SMethod m where
  SAdd :: SMethod Add
  SGet :: SMethod Get

perform :: SMethod m -> a -> ClientM (ResultOf m a) 以单例形式接收一个方法和一些值,并在Servant的perform monad中返回一个计算。此计算返回一个结果,该类型取决于方法和值的类型:如果我们ClientM,我们得到SAdd;如果我们AddResults SGet,我们会得到DictionaryNames。非常明智且非常自然 - 无需发明应用依赖类型的地方:任务需要大声地要求它们。

答案 1 :(得分:3)

这对我来说很奇怪,因为我的问题是到处都需要依赖类型。如果你没有看到,那么以这种方式看节目。

假设我们有f :: Ord a => [a] -> [a](我将使用Haskell表示法)。我们对此函数f了解多少?换句话说,您可以预测f []f [5,8,7]f [1,1,2,2]等应用程序?说你知道f x = [4,6,8]那你对x有什么看法?你可以观察到,我们知之甚少。

然后假设我告诉你f的真实姓名是sort。那么关于那些相同的例子你能告诉我什么?关于ysxs相关的f xs = ys,您能告诉我什么?现在你知道了很多,但这些信息来自哪里?我所做的只是改变功能的名称;这对于该计划的正式含义没有任何意义。

所有这些新信息都来自您对排序的了解。您知道两个明确的特征:

  1. sort xsxs
  2. 的排列
  3. sort xs单调增加。
  4. 我们可以使用依赖类型来证明sort的这两个属性。然后,这不仅仅是我们对分类的外在理解的问题;排序的意义成为该计划的内在组成部分。

    捕获错误是一种副作用。真正的目标是作为计划的一部分,在我们的头脑中指定和形式化我们必须知道的内容。

    仔细重新考虑您已编写的程序。什么使你的程序工作的事实只在你的头脑中知道?这些都是候选人的例子。