simonweijgers
simonweijgers

Reputation: 220

How to use type system to prevent nonsensical expressions

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

Answers (2)

kvb
kvb

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

Adam Kewley
Adam Kewley

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

Related Questions