好吧,因为您正在沿着Haskell路线行驶,所以要开始做一些事情:
您熟悉Curry-Howard的信件吗?有一些用于以此为基础的机器检查打样的系统,在许多方面,它们都是具有非常强大的类型系统的简单函数式编程语言。
您是否熟悉抽象数学领域,这些领域为分析Haskell代码提供了有用的概念?各种形式的代数和一些类别理论都出现了。
请记住,Haskell与所有图灵完备的语言一样,总是有可能终止。通常,要证明某事将永远是真实的,要比证明某事将是真实的或取决于一个非终止值要困难得多。
如果您正认真地寻求证明,而不仅仅是测试,那么这些就是要牢记的事情。基本规则是:使无效状态导致编译器错误。首先要防止对无效数据进行编码,然后让类型检查器为您完成繁琐的工作。
如果您想走得更远,如果记忆为我服务,证明助手Coq具有“提取到Haskell”功能,可让您证明关键函数的任意属性,然后将证明转换为Haskell代码。
对于直接在Haskell中做花式系统的工作,Oleg Kiselyov是大师。您可以在他的网站上找到一些示例,这些示例包括一些高级技巧,例如高级多态类型,用于对数组边界检查的静态证明进行编码。
对于更轻量级的内容,您可以执行诸如使用类型级别证书将某条数据标记为已检查正确性的操作。您仍然需要自己进行正确性检查,但是其他代码至少可以依赖于知道实际上已经检查了某些数据。
从轻量级验证和奇特类型系统技巧的基础上,您可以采取的另一步骤是利用Haskell作为宿主语言来嵌入特定于域的语言的效果很好;首先构造一个受严格限制的子语言(理想情况下,不是图灵完整的),您可以更轻松地证明该子语言的有用属性,然后使用该DSL中的程序在整个程序中提供关键的核心功能。例如,您可以证明两个参数的函数是关联的,以便证明使用该函数并行化项集合的合理化(因为函数应用程序的顺序无关紧要,仅参数的顺序无关紧要)。
哦,最后一件事。有关避免Haskell确实存在的陷阱的一些建议,这些陷阱可能会破坏原本可以安全地构建的代码:在这里,您发誓的敌人是一般递归,IO
monad和部分函数:
最后一个相对容易避免:不要编写它们,也不要使用它们。确保每个模式匹配都处理所有可能的情况,并且永远不要使用error
或undefined
。唯一棘手的部分是避免使用可能导致错误的标准库函数。有些显然是不安全的,例如fromJust :: Maybe a -> a
,head :: [a] -> a
但有些则可能更隐蔽。如果您发现自己编写的函数确实无法使用某些输入值做任何事情,那么您就可以通过输入类型对无效状态进行编码,并且需要首先对其进行修复。
第二种方法很容易在表面上避免,方法是通过各种纯函数散布东西,然后再从IO
表达式中使用它们。更好的是尽可能将整个程序移到纯代码中,以便可以使用除实际I / O之外的所有内容独立地对其进行评估。仅当您需要由外部输入驱动的递归时,这才变得棘手,这使我进入了最后一项:
明智的话:有充分根据的递归和生产性核心递归。始终确保递归函数要么从某个起点到某个已知的基本情况,要么按需生成一系列元素。在纯代码中,最简单的方法是通过递归折叠有限的数据结构(例如,而不是在将计数器增加到最大值之前直接调用自身的函数,创建一个保存计数器值范围并将其折叠的列表)或递归地生成惰性数据结构(例如,某个值的渐进逼近列表),同时请小心谨慎,切勿直接将二者混合使用(例如,不要只是“在满足某些条件的流中找到第一个元素”;它可能不会取而代之的是,从流中获取值直至某个最大深度,然后搜索有限列表,并适当地处理未找到的情况。
将最后两项结合起来,对于您真正需要IO
使用常规递归的部分,尝试将程序构建为增量组件,然后将所有尴尬的位浓缩为一个“驱动程序”功能。例如,您可以编写一个GUI事件循环,其中包含一个纯函数(例如)mainLoop :: UIState -> Events -> UIState
,退出测试quitMessage :: Events -> Bool
(一个),一个用于获取未决事件getEvents :: IO Events
的函数和一个update函数updateUI :: UIState -> IO ()
,然后使用诸如的通用函数实际运行该事件runLoopIO :: (b -> a -> b) -> b -> IO a -> (b -> IO ()) -> IO ()
。这使复杂的部分保持真正的纯净,使您可以使用事件脚本运行整个程序并检查结果UI状态,同时将笨拙的递归I / O部分隔离为一个易于理解且通常不可避免地正确的抽象函数。根据参数。