F#:将顶级函数转换为成员方法的优点?
早些时候,我就我的第一个F#项目征求了一些反馈意见。因为范围太大,所以在结束问题之前,有人很友好地检查了一下,并留下了一些反馈 他们提到的一件事是指出我有许多常规函数可以转换为数据类型上的方法。我尽职尽责地经历了一些变化,比如F#:将顶级函数转换为成员方法的优点?,f#,function,methods,F#,Function,Methods,早些时候,我就我的第一个F#项目征求了一些反馈意见。因为范围太大,所以在结束问题之前,有人很友好地检查了一下,并留下了一些反馈 他们提到的一件事是指出我有许多常规函数可以转换为数据类型上的方法。我尽职尽责地经历了一些变化,比如 let getDecisions hand = let (/=/) card1 card2 = matchValue card1 = matchValue card2 let canSplit() = let isPair() =
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更痛苦)。首先,我现在不再能够轻松地使用函数“流水线”。我不得不去改变一些链式的调用
到(一些链式的)。调用
等等。而且,它似乎使我的类型系统更加愚蠢——而在我的原始版本中,我的程序不需要类型注释,在大部分转换为成员方法之后,我遇到了一系列关于查找不确定的错误,我不得不添加类型注释(上面的(/=/)
就是一个例子)
我希望自己没有太多疑,因为我很感激收到的建议,而编写惯用代码对我来说很重要。我只是好奇为什么这个成语是这样的:)
谢谢 这是一个棘手的问题,两种方法各有优缺点。一般来说,我倾向于在编写更多面向对象/C风格的代码时使用成员,在编写更多功能代码时使用全局函数。然而,我不确定我是否能清楚地说明区别是什么 我更喜欢在编写以下内容时使用全局函数:
- 功能数据类型,例如树/列表和其他通常可用的数据结构,它们在程序中保留了大量数据。如果您的类型提供了一些表示为高阶函数的操作(例如,
),则可能就是这种数据类型List.map
- 与类型无关的功能-有些情况下,某些操作并不严格属于某一特定类型。例如,当您有某个数据结构的两种表示形式,并且正在实现这两种表示形式之间的转换(例如,编译器中的类型化AST和非类型化AST)。在这种情况下,函数是更好的选择
- 简单类型,例如
,可以具有属性(如值和颜色)和执行某些计算的相对简单(或没有)的方法卡片
卡
)并使用顶级函数实现应用程序的其余部分是完全可以的。事实上,我认为在你的情况下,这可能是最好的方法
值得注意的是,还可以创建一个类型,该类型包含成员以及提供与函数相同功能的模块。这允许库的用户选择他/她喜欢的样式,这是完全有效的方法(但是,对于独立库来说可能是有意义的)
作为补充说明,可以使用成员和函数实现调用链接:
// 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)
我在F#编程中使用的一种做法是在两个地方都使用代码 实际实现自然会放在一个模块中:
module Hand =
let getDecisions hand =
let (/=/) card1 card2 = matchValue card1 = matchValue card2
...
并在成员函数/属性中提供“链接”:
type Hand
member this.GetDecisions = Hand.getDecisions this
这种做法也通常用于其他F#库,例如powerpack中的矩阵和向量实现矩阵.fs
实际上,并非所有功能都应该放在两个位置。最终决策应基于领域知识
对于顶层,实践是将函数放在模块中,必要时将其中一些放在顶层。例如,在F#PowerPack中,
Matrix成员的一个优势是Intellisense和其他能够让成员发现的工具。当用户想要浏览对象时,他们可以键入foo.
,并获取类型上的方法列表。成员也更容易“扩展”,因为你不会在顶层有几十个名字;随着程序大小的增长,您需要更多的名称才能在限定时可用(someObj.Method或SomeNamespace.Type或SomeModule.func,而不仅仅是Method/Type/func“自由浮动”)
正如你所看到的,也有缺点;类型推断尤其值得注意(要调用x.Something
),您需要事先知道x的类型;对于非常常用的类型和功能,同时提供成员和函数模块可能会很有用,以便两者兼得(例如,FSharp.Core中的常见数据类型就是这样)
这些是“脚本编写方便性”与“软件工程规模”之间的典型权衡。就我个人而言,我总是倾向于后者。我通常避免上课,主要是因为成员们破坏类型推断的方式。如果您这样做,有时可以方便地使模块名与类型名相同-下面的编译器指令很方便
type Hand
// ...stuff...
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
let GetDecisions hand = //...
手动输入
//…东西。。。
[]
模块手=
让GetDecisions hand=/。。。
令人烦恼的是,您不能使用点符号将每个方法调用放在新行上。您可以
type Hand
// ...stuff...
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Hand =
let GetDecisions hand = //...