Using Types to Designing Domain I am trying to model a domain of metrics for use in a BI application.  I recently read the Designing with Types series, and felt that it could be useful, so I wanted to give it a try.  What I'm not sure is if I'm trying too hard....


The metrics are application-defined, meaning they are finite and won't change during runtime.  A dozen metrics would probably be a lot.
New metrics will be introduced as the application grows
A user will be selecting which metrics they want to use, and the application has to take that and go "do stuff", like building a query to the backend store (MongoDB, in this case).
There may be application logic associated with metrics that I would like enforced through pattern matching so that when I introduce a new metric, I want the compiler to tell me that I need to do some work.


Here's what I have:

type Metrics =
  | Revenue
  | Volume
  | PriceAsp
  with
    member m.FormatGroupings =
      match m with
        | Revenue -&gt; [("revenue", """$sum: { "$Amount" }" """)]
        | Volume  -&gt; [("volume", """$sum: { "$Quantity" }" """)]
        | PriceAsp -&gt; List.append Revenue.FormatGroupings Volume.FormatGroupings

    member m.FormatProjections =
      match m with
        | Revenue -&gt; [("revenue", "$revenue")]
        | Volume -&gt; [("volume", "$volume")]
        | PriceAsp -&gt; [("priceAsp", """ $divide: ["$revenue", "$volume"]  """)]


let buildQuery groupBy (metrics:Metrics list) =
  let concatenate f =
    let x = metrics
            |&gt; List.collect f
            |&gt; List.map (fun m -&gt; sprintf "{%s: %s}" (fst m) (snd m) )
    System.String.Join(",", x)

  let groupings = concatenate (fun m -&gt; m.FormatGroupings)
  let projections = concatenate (fun m -&gt; m.FormatProjections)

  sprintf """{$group: {_id: {%s: "$%s"}}, %s}, $project: {%s}}""" groupBy groupBy groupings projections


With examples of usage:

buildQuery "foo" [Revenue; Volume]  //Simple
buildQuery "foo" [PriceAsp]         //Compound


Note that PriceAsp is "made up of" both Revenue and Volume.

Please provide any general comments.  Some specific questions I have are:


Have the business rules for the metric calculations be embedded within the types
Show that some metrics are built off of other metrics (for example, PriceAsp), and enforce the explicit relationship.  Above, we see that PriceAsp is made up of Revenue and Volume because it's in that list.  But it would be better to make it enforced explicitly through typing.
There are formatting rules around an "instance" of a specific metric.  For example, when Revenue is shown to a user, it should be rounded to the nearest whole dollar, with thousand separators and a currency symbol (ex:  $1,234,567).
Is there an elegant way (using List.fold, perhaps) for doing what String.Join does?  String.Join is just so convenient...


A base metric would look like this (no $project):

{
  $group: {
    _id: {
      foo: "$Foo"
    },
    revenue: { $sum: "$Amount" }
  }
}


A compound metric may look like this:

{
  $group: {
    _id: {
      foo: "$Foo"
    },
    revenue: { $sum: "$Amount" },
    volume: { $sum: "$Quantity" }
  },
  $project: {
    priceAsp: { $divide: ["$revenue", "$volume"] }
  }
}
Do you have only a limited number of metric types?

In which case, you might want to make a union case of the possible types.
The compound cases can be recursive, referring to the type.

type Metric = 
    | Revenue of string * string 
    | Volume  of string * string
    | Compound  of string * Metric list


Then your formatMetric function needs to handle each case separately

let formatMetric metric =
    match metric with
    | Revenue (label,formula) -&gt; 
        sprintf "%s: {%s}" label formula
    | Volume  (label,formula) -&gt; 
        sprintf "%s: {%s}" label formula
    | Compound (label,metrics) -&gt; 
        sprintf "%s: {%s}" label "other metrics"


Note that that if you use this approach you don't really need the label any more, as the cases are the label.

type Metric = 
    | Revenue of string 
    | Volume  of string
    | Compound  of Metric list


And then you could also make formatMetric recursive, printing all the child formulas for a compound formula

let rec formatMetric metric =
    match metric with
    | Revenue formula -&gt; 
        sprintf "Revenue={%s}" formula
    | Volume  formula -&gt; 
        sprintf "Volume={%s}" formula
    | Compound metrics -&gt; 
        let childFormats = 
            metrics 
            |&gt; List.map formatMetric
            |&gt; List.reduce (+) 
        sprintf "Compound: {%s}" childFormats


The formatting is not exactly what you want, but I'm sure you get the general idea.

EDIT

General Comment

Personally, I wouldn't add those member functions to the type. I would focus on designing the types in their own module independent of any particular representation, and then have a separate module that mapped them into the appropriate representation for MongoDB.  That way you are not mixing up different requirements.  

String join.

A simple string join is trivial to write:

let stringJoin sep list :string = 
    let append s t = s + sep + t
    list |&gt; List.reduce append

stringJoin ", " ["a"; "b"; "c"; ]


Separate the types from the representation

But if you separate the types from the representation, you may find that you naturally create MongoDB specific functions that generate the appropriate representation. The idea is to build up a mini-language that you can compose together to make bigger blocks.
I don't know the MongoDB syntax, but maybe something like this?

let mSum p = sprintf "{ $sum: ""%s"" }" p
let mDivide p q = sprintf "{ $divide: [""%s"", ""%s""] }" p q
let mLabel p q = sprintf "%s : %s" s
let mGroup id ps = sprintf "$group: { %s, %s }" id (stringJoin ", " ps)
let mProject p q = sprintf "$project: { %s }" (stringJoin ", " ps)


If you are doing a lot of this, you might be better off using a JSON library. In which case the workflow would be:

domain type ==&gt;  convert to intermediate type for Mongo API ==&gt; convert to JSON