去年我问过" Dependent types can prove your code is correct up to a specification. But how do you prove the specification is correct?"。投票最多的答案提出了以下推理:
希望你的规范很简单,小到足以通过考试来判断,而你的实现可能要大得多。
这种推理对我来说很有意义。 Idris是测试这些概念最容易理解的语言;然而,由于它几乎可以像Haskell一样使用,它常常让程序员在旧概念中游荡,而不知道在哪里应用依赖类型。一些现实世界的例子可以对此有所帮助,因此,什么是好的,在实践中发生的程序的具体例子,很容易表达为类型,但实现起来很复杂?
答案 0 :(得分:4)
我会回答这个问题:
通常会让程序员在旧概念中游荡,而不知道在哪里应用依赖类型
确实类型可以用来消除某些类型的愚蠢错误,比如当你以错误的顺序将一个函数应用于它的参数时,但这不是真正适用的类型。类型构造您的推理并允许放大您的计算结构。
假设您处理列表并使用head
和tail
,但这些是部分功能,因此您决定切换到更安全的内容,现在处理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 r
和Operation
的定义如下:
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
。那么关于那些相同的例子你能告诉我什么?关于ys
与xs
相关的f xs = ys
,您能告诉我什么?现在你知道了很多,但这些信息来自哪里?我所做的只是改变功能的名称;这对于该计划的正式含义没有任何意义。
所有这些新信息都来自您对排序的了解。您知道两个明确的特征:
sort xs
是xs
。sort xs
单调增加。我们可以使用依赖类型来证明sort
的这两个属性。然后,这不仅仅是我们对分类的外在理解的问题;排序的意义成为该计划的内在组成部分。
捕获错误是一种副作用。真正的目标是作为计划的一部分,在我们的头脑中指定和形式化我们必须知道的内容。
仔细重新考虑您已编写的程序。什么使你的程序工作的事实只在你的头脑中知道?这些都是候选人的例子。