Reputation: 220
Is it possible to devise my types in such a way that I can write this:
let fieldValues = [nameField, VText "string"; ageField, VInteger 13]
but not this: (in the sense that it will be a compile time error):
let fieldValues = [nameField, VInteger 13; ageField, VText "string"]
type value =
| VText of string
| VInteger of int
type ty =
| TText
| TInteger
type field = { Id: int; Type: ty; Name: string }
let nameField = { Id=1; Type=TText; Name="Name" }
let ageField = { Id=2; Type=TInteger; Name="Age" }
Upvotes: 0
Views: 113
Reputation: 55184
It's not possible to do exactly what you want. However, here's something similar that will work:
type TypedField<'a> = { id : int; name : string }
type FieldConverter<'t> =
abstract Convert : TypedField<'a> * 'a -> 't
// Necessary only because F# type system won't let you implement FieldConverter<unit>
type FieldFunc =
abstract Use : TypedField<'a> * 'a -> unit
type Field =
abstract ApplyConverter : FieldConverter<'t> -> 't
abstract ApplyFunc : FieldFunc -> unit
let mkField field value =
{ new Field with
member __.ApplyConverter(f) = f.Convert(field, value)
member __.ApplyFunc(f) = f.Use(field, value) }
let nameField : TypedField<string> = { id = 1; name = "Name" }
let ageField : TypedField<int> = { id = 2; name = "Age" }
let fields = [mkField nameField "string"; mkField ageField 13]
// won't compile
let fields' = [mkField nameField 13; mkField ageField "string"]
Unfortunately, using the fields requires a fair amount of boilerplate:
// print names and values of all fields
fields
|> List.iter (fun f -> f.ApplyFunc { new FieldFunc with member __.Use(field, value) = printfn "%s: %O" field.name value })
Upvotes: 0
Reputation: 1234
The tuples in your list are of type value * ty
. For the compiler to notice you need the two connected you will need to let the compiler 'know' that you need distinct, connected, states. This may require you to remove some generic-ness:
type DataType =
| TextData of VText * { Id : int; Type : TText; Name : string }
| IntData of VInteger * { Id : int; Type : TInteger; Name : string }
You will then create a list of DataType
, the compiler will notice if you try to mix a VInteger into a TText record etc. Because you've explicitly stated the combinations in a discriminated union. The value
DI would be a little redundant:
type DataType =
| TextData of string * { Id : int; Type : string; Name : string }
| IntData of int * { Id : int; Type : int; Name : string }
Edit: (I'm in a pub typing this on a phone) you could clean it up with a generic also:
type DataType<'a> = {
Content : 'a * { Id : int; Type : 'a; Name : string }
}
type PossibleType = DataType<int> | DataType<string>
This probably isn't the ideal approach (others will have better); however, the principle I'm following here is that a compiler is only able to notice a relationship if it's stated. Clearly, this solution is only relatively clean for TypeA -> ValA
relationships and would become numerically ugly if you have many possible combinations (at which point you'd need to redesign the DI's as trees of all possibilities or refactor out variant data into a separate record).
Upvotes: 2