4

I have the following situation:

1. There are many (20 - 40) very similarly structured slowly changing data types.

2. Each data type at the low level is represented by a unique (for a type) string label and a unique (again, for a type) key (usually string or long). The key does not change, but the label (on average) may be updated less than once a year.

3. I "promoted" each such data type to F# DU, so that for each label/key there is a DU case. This is needed because some of the higher-level code needs to do a pattern matching. So, in a rare case when the types get updated, I want a compile error in case the things change. So, for each such type I generate/write the code like:

type CollegeSize = 
    | VerySmall
    | Small
    | Medium
    | Large
    | VeryLarge
with
    static member all = 
        [|
            (CollegeSize.VerySmall, "Very small (less than 2,000 students)", 1L)
            (CollegeSize.Small, "Small (from 2,001 to 5,000 students)", 2L)
            (CollegeSize.Medium, "Medium (from 5,001 to 10,000 students)", 3L)
            (CollegeSize.Large, "Large (from 10,000 to 20,000 students)", 4L)
            (CollegeSize.VeryLarge, "Very large (over 20,000 students)", 5L)
        |]

4. All such types are consumed in two places: F# engine, which does some calculations, and a web site. The F# engine just works with DU cases. The web sites these days, unfortunately, are mostly based on strings. So, the web team wanted a string based methods for almost any actions with the types and they are using C#.

5. So, I created two generic factories:

type CaseFactory<'C, 'L, 'K when 'C : comparison and 'L : comparison and 'K : comparison> = 
    {
        caseValue : 'C
        label : 'L
        key : 'K
    }

// DU factory: 'C - case, 'L - label, 'K - key
type UnionFactory< 'C, 'L, 'K when 'C : comparison and 'L : comparison and 'K : comparison> (all : array< 'C * 'L * 'K> ) =
    let map = all |> Array.map (fun (c, l, _) -> (c, l)) |> Map.ofArray
    let mapRev = all |> Array.map (fun (c, l, _) -> (l, c)) |> Map.ofArray
    let mapVal = all |> Array.map (fun (c, _, k) -> (c, k)) |> Map.ofArray
    let mapValRev = all |> Array.map (fun (c, _, k) -> (k, c)) |> Map.ofArray

    let allListValue : System.Collections.Generic.List<CaseFactory< 'C, 'L, 'K>> = 
        let x = 
            all 
            |> List.ofArray
            |> List.sortBy (fun (_, s, _) -> s)
            |> List.map (fun (c, s, v) -> { caseValue = c; label = s; key = v } : CaseFactory< 'C, 'L, 'K>)
        new System.Collections.Generic.List<CaseFactory< 'C, 'L, 'K>> (x)

    //Use key, NOT label to create.
    [<CompiledName("FromKey")>]
    member this.fromKey (k : 'K) : 'C = mapValRev.Item (k)

    // For integer keys you can pass "1" instead of 1
    [<CompiledName("FromKeyString")>]
    member this.fromKeyString (s : string) : 'C = this.fromKey (convert s)

     //Use key, NOT label to create.
    [<CompiledName("TryFromKey")>]
    member this.tryFromKey (k : 'K) : 'C option = mapValRev.TryFind k

    [<CompiledName("TryFromKeyString")>]
    member this.tryFromKeyString (s : string) : 'C option = mapValRev.TryFind (convert s)

     //Use key, NOT label to create.
    [<CompiledName("TryFromKey")>]
    member this.tryFromKey (k : 'K option) : 'C option = 
        match k with 
        | Some x -> this.tryFromKey x
        | None -> None

    [<CompiledName("AllList")>]
    member this.allList : System.Collections.Generic.List< CaseFactory< 'C, 'L, 'K>> = allListValue

    [<CompiledName("FromLabel")>]
    member this.fromLabel (l : 'L) : 'C = mapRev.[l]

    [<CompiledName("TryFromLabel")>]
    member this.tryFromLabel (l : 'L) : 'C option = mapRev.TryFind l

    [<CompiledName("TryFromLabel")>]
    member this.tryFromLabel (l : 'L option) : 'C option = 
        match l with 
        | Some x -> this.tryFromLabel x
        | None -> None

    [<CompiledName("GetLabel")>]
    member this.getLabel (c : 'C) : 'L = map.[c]

    [<CompiledName("GetKey")>]
    member this.getKey (c : 'C) : 'K = mapVal.[c]

6. At this point everything works as a clock if I create a generic factory with the singleton for each type like:

type CollegeSizeFactory private () =
    inherit UnionFactory<CollegeSize, string, int64> (CollegeSize.all)
    static let instance = CollegeSizeFactory ()

7. … except that web team does not want to use Instance each and every time when they call methods of a generic factory. So, I ended up writing:

type CollegeSizeFactory private () =
    inherit UnionFactory<CollegeSize, string, int64> (CollegeSize.all)
    static let instance = CollegeSizeFactory ()
    static member Instance = instance

    static member FromLabel s = CollegeSizeFactory.Instance.fromLabel s
    static member FromKey k = CollegeSizeFactory.Instance.fromKey k
    static member FromKeyString s = CollegeSizeFactory.Instance.fromKeyString s

The problem with that is that if tomorrow they need a few more shortcuts (like static member FromLabel s) then all implementations of generic factory must be updated by hands.

Ideally, I want a one-liner so that for each factory that I need I could just inherit from a generic type, like:

type CollegeSizeFactory private () =
    inherit UnionFactory<CollegeSize, string, int64> (CollegeSize.all)

and then automatically get everything that could be possibly needed, including all static members. I wonder if this is ever possible, and if yes, then how exactly.

Thanks a lot.

FoggyFinder
  • 2,230
  • 2
  • 20
  • 34
  • 2
    The question is way too long and complicated. Try to come up with a simpler, smaller example of what you need and what you have. Also, code formatting is all off. Also, the expression "работает как часы" doesn't directly translate to English. Try "like clockwork" or "like a charm". – Fyodor Soikin Dec 09 '17 at 03:48
  • 1
    Also, try to look at [statically resolved type parameters](https://stackoverflow.com/a/30305092/180286). – Fyodor Soikin Dec 09 '17 at 03:50

1 Answers1

3

I would suggest you drop the singleton idea, and instead use instance members, avoiding the problem altogether. Singletons are a bad idea to start with.

One approach would be to have a module with the factories, perhaps lazily evaluated (though I don't believe that's a concern in your scenario, they're not expensive to create). Then you could use that module directly, or maybe re-expose it through a static member on the type you want to create, e.g.

type CollegeSizeFactory () =
    inherit UnionFactory<CollegeSize, string, int64> (CollegeSize.all)

module Factories = 
    let collegeSize = lazy CollegeSizeFactory()

type CollegeSize with
    static member Factory = Factories.collegeSize.Value

CollegeSize.Factory.tryFromKey 1

The design space of your problem is rather large, and no doubt that's not the best solution to be found, but I feel it's a good first step that can be put in place with a simple refactoring.

But to be honest, you could probably get away with coding against your all members directly instead of precomputing those lookups. They are likely small enough that the performance difference doesn't justify maintaining this more complicated setup.

scrwtp
  • 13,437
  • 2
  • 26
  • 30