8 minute read

The first post in the domain specific language series explores the basics of domain modeling using record types in F#.

Introduction

A recent study1 found that programmers who used functional, statically-typed languages often started a programming task by constructing types to model their problem domain. I do too, but it’s how I start any complex task (even non-programming tasks). The ability to easily create types is partially what drew me to F# in the first place. I use static types as a lightweight specification language; I can designate high level functions with their anticipated signatures (using types that make sense within the domain) and then the compiler tells me when I stray from that specification as I implement.

Mitch Hedberg

Study domain

To motivate this series of posts I chose a similar domain to that used in the book DSLs in Action. But since I don’t have a book’s worth of content I will only adopt a subset of this domain to demonstrate my examples. We will model a simplified stock transaction.

Simple transaction type
Buy/Sell discriminated union
Which stock? discriminated union
Price? decimal
How many? int
Partial/AllOrNone bool

Data model #1 (Record type)

We start by modeling the domain with an F# Record type. Let me say for the record that I use F# record types 98% of the time. I rarely have a good reason to use anything else. Here is one way to create the record type.

type Transaction = Buy | Sell

type Stocks = MSFT | GOOGL | META

type Trade = {
        buyOrSell: Transaction
        ticker: Stocks
        numShares: int
        price: decimal
        allOrNone: bool }

And this is what a list of trades would look like:

let (trades: Trade list) = 
  [
    {buyOrSell = Buy; ticker = MSFT; numShares = 4; price = 258.32m; allOrNone=true};
    {buyOrSell = Sell; ticker = META; numShares = 3; price = 158.71m; allOrNone=true};
    {buyOrSell = Sell; ticker = GOOGL; numShares = 6; price = 106.08m; allOrNone=true};    
  ]

Trade Module

We will use the following functions throughout the remainder of this series. The last function tradeMany will be used to process an order (i.e. a list of Trades).

module Trade = 

  let buyOne order = 
    let totalPrice = order.price * decimal(order.numShares)
    printfn $"You just purchased {order.numShares} shares of {order.ticker} for a total cost of ${totalPrice:N2}." 
    (-1.0m * totalPrice)

  let sellOne order = 
    let totalPrice = order.price * decimal(order.numShares)
    printfn $"You just sold {order.numShares} shares of {order.ticker} for a total earnings of ${totalPrice:N2}." 
    totalPrice

  let tradeOne order =   
    match order.buyOrSell with
    | Buy -> (buyOne order)          
    | Sell -> (sellOne order)

  let tradeMany order = 
    let totalOrderPrice = 
      order
      |> List.map tradeOne
      |> List.sum
    printfn $"-------------------------------------------------------------------"
    match (totalOrderPrice > 0.0m) with
    | true -> printfn $"You just executed a series of trades that earned you ${totalOrderPrice:N2}."
    | false -> printfn $"You just executed a series of trades that cost you ${totalOrderPrice:N2}."

And this is the output if you run Trade.tradeMany trades:

You just purchased 4 shares of MSFT for a total cost of $1,033.28.
You just sold 3 shares of META for a total earnings of $476.13.
You just sold 6 shares of GOOGL for a total earnings of $636.48.
-------------------------------------------------------------------
You just executed a series of trades that earned you $79.33.

The code listing for this series can be found here.

Next

In the next post we will see how to use F# computation expressions to create identical Trade records.

Footnotes

  1. Justin Lubin. 2021. How Statically-Typed Functional Programmers Author Code. In Extended Abstracts of the 2021 CHI Conference on Human Factors in Computing Systems (CHI EA ‘21). Association for Computing Machinery, New York, NY, USA, Article 484, 1–6. https://doi.org/10.1145/3411763.3451515 

Tags: , ,

Updated: