之前我曾就我的第一个F#项目请求了一些反馈。在结束问题之前,因为范围太大,有人很友好地查看并留下一些反馈。
他们提到的一件事是指出我有许多常规函数可以转换为我的数据类型的方法。我尽职尽责地改变了像
这样的事情let getDecisions hand =
let (/=/) card1 card2 = matchValue card1 = matchValue card2
let canSplit() =
let isPair() =
match hand.Cards with
| card1 :: card2 :: [] when card1 /=/ card2 -> true
| _ -> false
not (hasState Splitting hand) && isPair()
let decisions = [Hit; Stand]
let split = if canSplit() then [Split] else []
let doubleDown = if hasState Initial hand then [DoubleDown] else []
decisions @ split @ doubleDown
到此:
type Hand
// ...stuff...
member hand.GetDecisions =
let (/=/) (c1 : Card) (c2 : Card) = c1.MatchValue = c2.MatchValue
let canSplit() =
let isPair() =
match hand.Cards with
| card1 :: card2 :: [] when card1 /=/ card2 -> true
| _ -> false
not (hand.HasState Splitting) && isPair()
let decisions = [Hit; Stand]
let split = if canSplit() then [Split] else []
let doubleDown = if hand.HasState Initial then [DoubleDown] else []
decisions @ split @ doubleDown
现在,我不怀疑我是一个白痴,但除了(我猜)让C#interop变得更容易,这是什么让我受益?具体来说,我发现了一些 dis 的优点,不计算转换的额外工作(我不会计算,因为我可以这样做,首先,我想,虽然那样做已经使用F#Interactive更多的是痛苦)。首先,我现在不再能够轻松地使用“流水线”功能了。我不得不把some |> chained |> calls
更改为(some |> chained).Calls
等等。此外,它似乎使我的类型系统变得笨重 - 而对于我的原始版本,我的程序在转换为成员方法之后不需要类型注释,我得到了一堆关于查找在这一点上不确定的错误,我不得不去添加类型注释(这个例子在上面的(/=/)
中)。
我希望我没有太过可疑,因为我很欣赏我收到的建议,而且编写惯用代码对我来说非常重要。我只是好奇为什么这个成语是这样的:)
谢谢!
答案 0 :(得分:7)
我在F#编程中使用的一种做法是在两个地方使用代码。
实际的实现很自然地放在一个模块中:
module Hand =
let getDecisions hand =
let (/=/) card1 card2 = matchValue card1 = matchValue card2
...
并在成员函数/属性中提供“链接”:
type Hand
member this.GetDecisions = Hand.getDecisions this
这种做法通常也用于其他F#库,例如: powerpack中的矩阵和向量实现matrix.fs。
在实践中,并非每个功能都应放在两个地方。最终决定应基于领域知识。
关于顶级,实践是将函数放入模块中,并在必要时将其中一些放在顶层。例如。在F#PowerPack中,Matrix<'T>
位于名称空间Microsoft.FSharp.Math
中。在顶层中为矩阵构造函数创建快捷方式很方便,这样用户可以直接构造矩阵而无需打开命名空间:
[<AutoOpen>]
module MatrixTopLevelOperators =
let matrix ll = Microsoft.FSharp.Math.Matrix.ofSeq ll
答案 1 :(得分:4)
成员的优势是智能感知和其他工具,使成员可被发现。当用户想要探索对象foo
时,他们可以键入foo.
并获取该类型的方法列表。会员们的“规模”也更容易,因为你不会在顶层浮动数十个名字;随着程序大小的增加,您需要更多名称才能在限定时使用(someObj.Method或SomeNamespace.Type或SomeModule.func,而不仅仅是方法/类型/ func'浮动免费')。
如你所见,也有缺点;类型推断尤其值得注意(您需要知道调用x
的先验x.Something
的类型);在非常普遍使用的类型和功能的情况下,提供成员和函数模块可能是有用的,以获得两者的好处(例如,对于FSharp.Core中的常见数据类型会发生什么)。 / p>
这些是“脚本方便”与“软件工程规模”的典型权衡。我个人总是倾向于后者。
答案 2 :(得分:3)
这是一个棘手的问题,两种方法都有优点和缺点。通常,在编写更多功能代码时,我倾向于使用成员编写更多面向对象/ C#样式的代码和全局函数。但是,我不确定我能说清楚它们之间的区别。
我更喜欢在写作时使用全局函数:
功能数据类型,例如树/列表和其他通常可用的数据结构,可以在程序中保留大量数据。如果您的类型提供了一些表示为高阶函数的操作(例如List.map
),那么它可能就是这种数据类型。
与类型无关的功能 - 有些操作并非严格属于某种特定类型。例如,当您对某些数据结构有两个表示,并且您正在实现两者之间的转换时(例如,在编译器中键入AST和无类型AST)。在这种情况下,功能是更好的选择。
另一方面,我会在写作时使用会员:
简单类型,例如Card
,可以具有属性(如值和颜色)和相对简单(或没有)方法执行某些计算。
面向对象 - 当您需要使用面向对象的概念(如接口)时,您需要将功能编写为成员,因此考虑它可能会有用(提前)您的类型是否属于这种情况。
我还要提到,在应用程序中使用成员来处理某些简单类型(例如Card
)并使用顶级函数实现应用程序的其余部分是完全正常的。事实上,我认为这可能是您遇到的最佳方法。
值得注意的是,还可以创建一个类型,其中包含成员以及提供与函数相同功能的模块。这允许库的用户选择他/她喜欢的样式,这是完全有效的方法(但是,它可能对独立库有意义。)
作为旁注,可以使用成员和函数来实现对呼叫的链接:
// Using LINQ-like extension methods
list.Where(fun a -> a%3 = 0).Select(fun a -> a * a)
// Using F# list-processing functions
list |> List.filter (fun a -> a%3 = 0) |> List.map (fun a -> a * a)
答案 3 :(得分:1)
我通常主要是因为成员削弱类型推理的方式而避免使用类。如果你这样做,有时候使模块名称与类型名称相同是很方便的 - 下面的编译器指令就派上用场了。
type Hand
// ...stuff...
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
let GetDecisions hand = //...