F# 工作流/开发流程

8
(我使用“工作流程”一词 - 不是指异步工作流程的意义 - 而是在“git工作流程”的意义上,即您如何将其作为开发的一部分使用)
玩了一段时间的F#后,我开始开发我的第一个F#应用程序。我来自c#/vb。在观看各种演示/讲座之后 - 不管是对还是错 - 我开始使用fsi作为主要开发“引擎”,并在该领域内开展工作。如果我遇到需要调试的问题,我倾向于将有问题的函数拆分成较小的部分,并检查这些部分是否正常工作以尝试调试问题。
然而,为了使fsi中的代码量可管理,一旦我对所做的内容满意,我就将其移动到一个.fs文件中,并且使用#load将.fs文件加载回fsi。随着应用程序变得越来越大,这可能开始感觉有些笨重,因为当我需要重构时,我不得不重新引入fs文件中的内容并更改运行的内容,以便再次使代码正常工作,然后将代码推回到.fs文件中。此外,这种风格并不是真正的测试优先方法,因此我没有获得建立测试集的好处。(我还可能错过设置断点/步进代码的能力,这在某些情况下例如递归,可以比分解函数部分更快地诊断错误-虽然也许这在VS11中是可用的,但我没有正确设置)...所以我认为我可能没有最优化地做事情,或者没有以正确的方式考虑事情。
我想知道其他人如何开发应用程序。您主要使用fsi还是从tdd开始?应该将tdd方法作为主要的开发工具,FSI仅在实现更复杂的算法,数据探索等方面使用吗?
我看过了这个问题,显然它有助于指向各种F#的tdd框架,但我仍然很想了解经验丰富的F#开发人员的工作流程。
非常感谢。

S


你有试过脚本文件吗?这似乎是一个很好的机会,在此过程中添加一些测试。 - Artem Koshelev
谢谢。你能否就TDD方面再详细解释一下?我一直把它们仅仅当作在FSI中执行代码的“自由”方式。显然我漏掉了什么! - Simon Woods
REPL并不能替代适当的测试,它们应该相辅相成。https://twitter.com/missingfaktor/status/314452078682595328 - Mauricio Scheffer
2个回答

12

我认为你走在了正确的轨道上。

开发流程是一种品味。但我还是会分享我的方法。

  • 先创建几个fs文件。每个文件代表一个模块,由一组紧密相关的函数组成。开始时不必太严格,你可以随时调整模块间的内容。
  • 创建几个fsx文件,方便快速测试模块骨架。
  • 创建一个测试项目并设置NuGet包。我经常同时使用 NUnit 和FsUnit
  • 只要fsx脚本给出正确结果,就将其转移到测试用例中。 重复此步骤。
  • Program.fs包含在主项目中,并编译为可执行文件以进行需要的调试。

总体来说,F# REPL是主要的开发引擎。它能给我即时反馈并允许增量更改,这对原型设计非常有帮助。在F#中,TDD不如其他语言那样关键,因为错误率低得多。我不会测试所有东西,只关注主要功能并确保高测试覆盖率。使用testdriven.net插件或Visual Studio 2012的Premium和Ultimate版本可以为你提供有用的测试覆盖率统计数据。

使用F# REPL和TDD,我几乎不需要使用调试。每当出现错误行为时,我会停下来思考。由于你的代码没有副作用,你可以很容易地推理它们。在许多情况下,推理加上几个打印命令就可以给出正确答案。

你可以使用UnquoteFsCheck在F# REPL中进行TDD。前者通过引用提供测试,非常出色。后者使用随机测试方法,可处理代码的边角情况,具有吸引力。我发现当程序必须满足特定属性时,它非常有用。然而,学习如何正确使用这些框架可能需要时间。


3

pad 给出了一个非常实用的答案,对于刚接触 F# 的人来说非常有帮助。我将提供一种不同的方法,以便其他人不会认为 F# 只有一种方式。

注意:如果你是编程新手,请坚持使用 pad 的答案,它对于新手程序员来说更好。

面向对象的世界中,人们通过对象进行思考,在这样的语言中,我会从纸上写下对象,并使用各种图表(例如用例状态转换序列图等)进行工作,直到我感觉已经有足够的东西开始在 C# cs 文件中创建对象,完善对象的方法、属性、事件等。

在函数式编程中,我通常从抽象概念开始,并将它们转换为 F# fs 文件中的判别联合(DU),跳过使用REPL(即F# Interactive),然后开始添加一些函数。当我有了几个函数后,我使用NuGet设置一个测试项目,通过NUnitFsUnit进行测试。由于 DU 是抽象的,所以测试用例通常更难编写,因此我为 DU 创建打印机,然后将其插入到测试用例中,在 NUnit 工具中捕获打印机的结果输出,以便剪切和粘贴回测试用例并进行必要的更改。请参见这些示例,了解为什么我不手动从头开始编写它们。
一旦我完成了抽象的DU,我就可以开始编写代码,将人类/具体形式转换为抽象的DU,并将抽象的DU转换为人类/具体形式。在某些情况下,这些会是解析器美化打印机
我想要表达的主要观点是,我不关注我使用的工具,而是关注问题的抽象概念,并在需要时引入工具。
我还要注意,我也会在PROLOG中编程,在那里我从REPL开始,一旦逻辑可行就将代码移到存储库中。因此,我并不反对使用REPL,这只是一种不同的解决问题的方式。
编辑
根据肯恩的要求,举一个例子。
请参见:F#中的判别联合类型,并查找以下章节:使用判别联合类型代替对象继承 因此,不再使用基本类型形状及其继承类型圆形、等边三角形、正方形和矩形,而是创建一个判别联合类型,如下所示:
type Shape =
| Circle of float
| EquilateralTriangle of double
| Square of double
| Rectangle of double * double

作为一个独立问题,你的问题会得到更好的回答,比我能给出的更详细,建议你提问。
此外,如果你搜索这个信息,也可以用以下替代项搜索区分联合(DU):
- 代数数据类型 - 广义代数数据类型(GADT) - 标记联合 - 变体 - 变体记录 - 不相交联合 - 总和类型

谢谢虚拟机。那里有一点需要消化 - 特别是例子!如果我没记错的话,您是在说当您开始在C#中创建对象层次结构时,大概会创建DUs,或者您是在说一些略微不同的东西?此外,当您说“为DU创建打印机” - StructuredFormatDisplayAttribute时,您是否有一个示例? - Simon Woods
1
“我这样理解你是在想,在使用C#编程时创建对象层次结构的起点大致与创建DUs相同,对吗?” “是的,基本上是这样。面向对象编程优先考虑对象,但函数式编程优先考虑函数。但是在编写函数之前,需要一些类型,因此在超出基础范围时,你可能需要一个DU。注意:可以进行不需要DU的项目,因为它都是基本类型的函数,所以不要认为总是需要DU,就像在面向对象的语言中解决问题时可能不需要对象一样。” - Guy Coder
1
另外,当你说“为DU创建打印机”时,你有没有一个例子?这实际上是一个非常好的问题,你应该在这里向其他人提问。我会给你指一些地方,但是请认真询问,因为其他人可以提供更多信息。请参见:fol.fs 你还需要了解AddPrinter。请参见:fol.fsx - Guy Coder
1
另请参见:intro.fsintro.fsx,以获取一些更简单的打印机。您还应该重置F#交互,打印一些DU值,然后运行AddPrinter语句并再次打印,以查看它所做出的差异。 - Guy Coder
你能提供一个例子来支持你的回答吗?(我不明白DUs如何帮助你的早期设计阶段)。 - MasterMastic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接