Search code examples
genericsf#algebraic-data-typesdiscriminated-union

How Can I Restrict the Usage of an F# Union Type to A Particular Option


I am teaching myself F#--For Fun and Profit!--and, while I've made some strides, I have run into a stumbling block with usage of algebraic types. Below is a JSON type that I coded to serialize an arbitrary JSON structure to a string. I am open to subjective comments on it's design and efficiency, of course, but I am mainly focussed on line 7:

type JSON =
    | JString of string
    | JNumber of decimal
    | JBool   of bool
    | JNull
    | JArray  of JSON list
    | JObject of Map< JSON, JSON >
with
    member this.Serialize  =
        let rec serialize ( element : JSON ) =
            match element with
            | JString str ->
                "\"" + str + "\""
            | JNumber num ->
                num.ToString()
            | JBool   bln -> 
                bln.ToString().ToLower()
            | JNull       ->
                "null"
            | JArray  ary ->
                "[" + String.concat "," ( List.map serialize ary ) + "]"
            | JObject obj -> 
                "{" + (
                    Map.fold (
                        fun state key value ->
                            state + ( match state with "" -> "" | _ -> "," )
                                  + ( serialize key ) 
                                  + ":" 
                                  + ( serialize value ) ) "" obj ) + "}"
        serialize( this )

Anyone familiar with JSON knows that a key/value pair of a JSON object should be keyed on a string, not just any JSON element/value. Is there a way to further restrict the first type parameter of the Map? These, of course, do not work:

type JSON =
    ... elided ...
    | JObject of Map< JSON.JString, JSON >

...

type JSON =
    ... elided ...
    | JObject of Map< JString, JSON >

...

type JSON =
    ... elided ...
    | JObject of Map< string, JSON >

Thanks.


Solution

  • Referencing another case identifier from within a discriminated union is not possible. From Discriminated Unions,

    Syntax

    [ attributes ]

    type [accessibility-modifier] type-name =

        | case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 ...]

        | case-identifier2 [of [fieldname3 : ] type3 [ * [ fieldname4 : ] type4 ...]

        [ member-list ]

    This means that each case identifier must be of some type. A case identifier itself is not a type.

    One way you could achieve the same functionality is by breaking the discriminated union into multiple discriminated unions:

    type JSONKey =
    | JString of string
    
    type JSONValue =
    | JString of string
    | JNumber of decimal
    | JBool of bool
    | JNull
    | JArray of JSONValue list
    | JObject of Map<JSONKey, JSONValue>
    

    and then defining JSON as:

    type JSON = Map<JSONKey, JSONValue>
    

    Then, serialize would need to be changed to let rec serialize ( element : JSONValue ) and serialize( this ) would need to be changed to serialize( JObject this ).


    As @Ringil mentioned, Map<string, JSON> will work in this situation, but this is not too extensible/restrictive.