Search code examples
f#record

Why doesn't F# infer the record type when updating a record?


This example code:

type recordA = { X: string; }
type recordB = { X: string; }

let modifyX newX record = { record with X = newX }

let modifiedRecordA = {recordA.X = "X"} |> modifyX "X2" 
let modifiedRecordB = {recordB.X = "X"} |> modifyX "X2" 

Results in:

  let modifyX newX record = { record with X = newX }
  --------------------------^^^^^^^^^^^^^^^^^^^^^^^^

stdin(4,27): warning FS0667: The field labels and expected type of this record expression or pattern do not uniquely determine a corresponding record type

  let modifiedRecordA = {recordA.X = "X"} |> modifyX "X2" 
  -------------------------------------------^^^^^^^^^^^^

stdin(6,44): error FS0001: Type mismatch. Expecting a
    recordA -> 'a    
but given a
    recordB -> recordB    
The type 'recordA' does not match the type 'recordB'

My expectation is that modifiedRecordA ends up equivalent to { recordA.X = "X2" } and modifiedRecordB ends up equivalent to { recordB.X = "X2" }, but it doesn't seem to work that way.

  1. Why doesn't this just infer and return the appropriate record type based on the parameter type?
  2. Is there anything I can do to make this work?

Solution

  • The inline magic required to make this work is based on overload resolution combined with statically resolved member constraints. Overloads defined as operators avoid the need to spell out explicit constraints.

    type Foo = Foo with
        static member ($) (Foo, record) = fun newX -> { record with recordA.X = newX }
        static member ($) (Foo, record) = fun newX -> { record with recordB.X = newX }
    
    let inline modifyX newX record = (Foo $ record) newX
    
    let modifiedRecordA = {recordA.X = "X"} |> modifyX "X2" 
    let modifiedRecordB = {recordB.X = "X"} |> modifyX "X2"
    

    Constructs which pass types for which no overload exists do not compile.

    type recordC = { X: string; }
    let modifiedRecordC = {recordC.X = "X"} |> modifyX "X2" 
    // Error    No overloads match for method 'op_Dollar' ...
    // Possible overload ... The type 'recordC' is not compatible with the type 'recordB'
    // Possible overload ... The type 'recordC' is not compatible with the type 'recordA'
    

    This is not really intended for actual use. Heed the advise and explore if other approaches are better suited to your problem.