F# 中的类型与模块

Types vs. Modules in 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 只是不想放弃 classes(尽管当我们离开 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

所以对象会变得更简单,最终可以变成一个简单的存储类型,无需任何扩充。

但我想知道有什么实际好处(让我们只考虑可读性、可维护性等)?

此外,我看不出如何将其重新构造为不是 class,缺少模块中的 'internal' class 并执行所有操作通过那个,但这只会改变它。

您关于将 LowAllowedPriceDeviation 转为模块的直觉是正确的:它可以成为将 this 参数移至末尾的函数。这是一个公认的模式。

Instrument 类型的所有其他方法也是如此。这两个私有静态方法可以成为模块中的私有函数。完全相同的方法。

问题 “如何将其重新构造为不是 class” 让我有点困惑,因为这实际上不是 class. Instrument 是一条记录,不是 class。事实上,你给了它一些实例和静态方法并不能使它成为 class.

最后(尽管从技术上讲,这部分是基于意见的),关于 “实际好处是什么” - 答案是“可组合性”。函数可以用方法不能的方式组合。

例如,假设您想要一种打印多个仪器的方法:

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())

这只是函数可组合性的一个例子,还有很多其他例子。但是我不想就这个主题写一整篇博客 post,它已经比我想要的长很多了。