Thomas
Thomas

Reputation: 12107

Types vs. Modules in F#

The answer on Confused about static dictionary in a type, in F# finished with one advice: 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

Which is a great point, but my 30 years of OO just don't want to give up classes just yet (although I was fighting against C++ like crazy when we moved away from C...)

so let's take a practical real world object:

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

In this example, there is an Instrument object with its data and a few helper members and a module which acts as a repository when it's time to find an object by name (a trading ticker in this case, so they're known and formatted, it's not a random string)

I could move the helping member to the module, for example:

member this.LowAllowedPriceDeviation (basePrice: decimal)  = this.MinPriceMultiplier * basePrice

could become:

let lowAllowedPriceDeviation basePrice instrument = instrument.MinPriceMultiplier * basePrice

So the object would become simpler and could eventually be turned into a simple storage type without any augmentations.

But I am wondering what are the practical benefits (let's just consider readability, maintainability, etc)?

Also, I don't see how this could be re-structured to not be a class, short of having an 'internal' class in the module and doing all operations through that, but that would just be shifting it.

Upvotes: 1

Views: 215

Answers (1)

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

Your intuition about turning LowAllowedPriceDeviation to a module is correct: it could become a function with the this parameter moved to the end. That is an accepted pattern.

Same goes for all other methods on the Instrument type. And the two private static methods could be come private functions in the module. The exact same approach.

The question "how this could be re-structured to not be a class" confuses me a bit, because this is not actually a class. Instrument is a record, not a class. The fact that you gave it some instance and static methods doesn't make it a class.

And finally (though, technically, this part is opinion-based), regarding "what are the practical benefits" - the answer is "composability". Functions can compose in the way that methods can't.

For example, say you wanted a way to print multiple instruments:

let printAll toString = List.iter (printfn "%s" << toString)

See how it's parametrized with a toString function? That's because I'd like to use it for printing instruments in different ways. For example, I might print their prices:

printAll priceToString (list())

But if PriceToString is a method, I'd have to introduce an anonymous function:

printAll (fun i -> i.PriceToString) (list())

This looks just a little bit more involved than using a function, but in practice it gets very complicated fast. A bigger problem, however, is that this wouldn't even compile because type inference doesn't work on properties (because it can't). In order to get it to compile, you have to add a type annotation, making it even uglier:

printAll (fun (i: Instrument) -> i.PriceToString) (list())

That's just one example of function composability, there are many others. But I'd rather not write a whole blog post on this subject, it's already much longer than I'd like.

Upvotes: 5

Related Questions