F#中的类型与模块
f#
关于类型中的静态字典的困惑的答案,在 F# 中完成了一个建议:and just in general: try to use fewer classes and more modules and functions; they're more idiomatic in F# and lead to fewer problems in general
这是一个很好的观点,但我 30 年的 OO 只是还不想放弃课程(尽管当我们离开 C 时,我疯狂地与 C++ 作斗争......)
所以让我们拿一个实际的现实世界对象:
type Currency =
{
Ticker: string
Symbol: char
}
and MarginBracket =
{
MinSize: decimal
MaxSize: decimal
Leverage: int
InitialMargin: decimal
MaintenanceMargin: decimal
}
and Instrument =
{
Ticker: string
QuantityTickSize: int
PriceTickSize: int
BaseCurrency: Currency
QuoteCurrency: Currency
MinQuantity: decimal
MaxQuantity: decimal
MaxPriceMultiplier: decimal
MinPriceMultiplier: decimal
MarginBrackets: MarginBracket array
}
// formatting
static member private formatValueNoSign (precision: int) (value: decimal) =
let zeros = String.replicate precision "0"
String.Format($"{{0:#.%s{zeros}}}", value)
static member private formatValueSign (precision: int) (value: decimal) =
let zeros = String.replicate precision "0"
String.Format($"{{0:+#.%s{zeros};-#.%s{zeros}; 0.%s{zeros}}}", value)
member this.BaseSymbol = this.BaseCurrency.Symbol
member this.QuoteSymbol = this.QuoteCurrency.Symbol
member this.QuantityToString (quantity) = $"{this.BaseSymbol}{Instrument.formatValueSign this.QuantityTickSize quantity}"
member this.PriceToString (price) = $"{this.QuoteSymbol}{Instrument.formatValueNoSign this.PriceTickSize price}"
member this.SignedPriceToString (price) = $"{this.QuoteSymbol}{Instrument.formatValueSign this.PriceTickSize price}"
member this.RoundQuantity (quantity: decimal) = Math.Round (quantity, this.QuantityTickSize)
member this.RoundPrice (price : decimal) = Math.Round (price, this.PriceTickSize)
// price deviation allowed from instrument price
member this.LowAllowedPriceDeviation (basePrice: decimal) = this.MinPriceMultiplier * basePrice
member this.HighAllowedPriceDeviation (basePrice: decimal) = this.MaxPriceMultiplier * basePrice
module Instrument =
let private allInstruments = Dictionary<string, Instrument>()
let list () = allInstruments.Values
let register (instrument) = allInstruments.[instrument.Ticker] <- instrument
let exists (ticker: string) = allInstruments.ContainsKey (ticker.ToUpper())
let find (ticker: string) = allInstruments.[ticker.ToUpper()]
在这个例子中,有一个带有数据的Instrument对象和一些帮助成员,还有一个模块,当需要按名称查找对象时充当存储库(在本例中为交易代码,因此它们是已知的和格式化的,它不是一个随机字符串)
我可以将帮助成员移动到模块,例如:
member this.LowAllowedPriceDeviation (basePrice: decimal) = this.MinPriceMultiplier * basePrice
可以变成:
let lowAllowedPriceDeviation basePrice instrument = instrument.MinPriceMultiplier * basePrice
所以对象会变得更简单,最终可以变成一个简单的存储类型,没有任何增强。
但我想知道实际好处是什么(让我们只考虑可读性、可维护性等)?
此外,我不知道如何将其重新构建为不是一个类,缺少模块中的“内部”类并通过它执行所有操作,但这只会改变它。
回答
您关于转向LowAllowedPriceDeviation模块的直觉是正确的:它可以成为一个将this参数移到最后的函数。这是一种公认的模式。
Instrument类型上的所有其他方法也是如此。并且这两个私有静态方法可以成为模块中的私有函数。完全相同的方法。
现在的问题“这怎么会被重新构造成不是一个类”让我困惑了一下,因为这实际上不是一个类。Instrument是一个记录,而不是一个类。你给它一些实例和静态方法的事实并没有使它成为一个类。
最后(虽然,从技术上讲,这部分是基于意见的),关于“实际好处是什么” - 答案是“可组合性”。函数可以以方法不能的方式组合。
例如,假设您想要一种打印多种乐器的方法:
let printAll toString = List.iter (printfn "%s" << toString)
看看它是如何用toString函数参数化的?那是因为我想以不同的方式将它用于打印仪器。例如,我可能会打印他们的价格:
printAll priceToString (list())
但如果PriceToString是一种方法,我必须引入一个匿名函数:
printAll (fun i -> i.PriceToString) (list())
这看起来比使用函数要复杂一些,但实际上它很快就会变得非常复杂。然而,更大的问题是这甚至无法编译,因为类型推断对属性不起作用(因为它不能)。为了让它编译,你必须添加一个类型注释,使它更难看:
printAll (fun (i: Instrument) -> i.PriceToString) (list())
这只是函数可组合性的一个例子,还有很多其他例子。但我宁愿不写一篇关于这个主题的完整博客文章,它已经比我想要的要长得多。