Generically add field to anonymous record using a function

250 Views Asked by At

I would like to be able to take an arbitrary record as a parameter and return an anonymous record with a field added using the copy-and-update syntax.

For example, this works:

let fooBar =
  {| Foo = ()
     Bar = () |}

let fooBarBaz = {| fooBar with Baz = () |}

But I would like to do this:

let fooBar =
  {| Foo = ()
     Bar = () |}

let inline addBaz a = {| a with Baz = () |} (* The input to a copy-and-update expression that creates an anonymous record must be either an anonymous record or a record *)

let fooBarBaz = addBaz fooBar

Is there a way to do this in F#?

2

There are 2 best solutions below

10
Gus On BEST ANSWER

No, that's not possible.

Think about it, if that function was possible, what would be the type ? Is it something that "fits" into existing F# type system?

The type should be something like val addField: x: {| FieldName<1>: 't1; FieldName<2>: 't2; ... FieldName<n>: 'tn;|} -> {| FieldName<1>: 't1; FieldName<2>: 't2; ... FieldName<n>: 'tn; Baz: unit |}

Clearly something like that's not representable in F# type system.

UPDATE

It was mentioned that adding a record constraint would allow this but this is quite far from reality. A record constraint would just filter the argument to be a record but the problem which remains is how do the type system express that the function takes a type ^T when ^T : record and returns something like ^U when ^U : ^T_butWithAnAdditionalField

Also, on the SRTP side there is a way to "read" a field by adding a get_FieldName constraint but not to write, moreover the possibility to read a field allow us to read a field of a known name, not any name.

Conclusion: F# type system is very far away from allowing to express stuff like this and the SRTP mechanism is not there either.

Allowing an "is record" constraint shouldn't be that complicated but it won't solve anything here.

12
Tom Moers On

This is currently not possible. You can find a discussion about it in this proposal:

#807: Allow record to be a generic constraint

If I understand correctly, the solution they mention in the proposal is that you have to be able to constrain the input type to be record. Something that is currently not possible.

But as Gus points out in his answer, a second consideration is that you also need to have a way to specify the output type because the output type depends on the input type.

UPDATE:

There is some discussion going on whether the linked to proposal is relevant or not or if the conclusion I draw from it it correct. First of all, I do not work on the compiler, so I don't know what the biggest hurdle would be. But below analysis does not seem unreasonable to me.

First, let's get rid of generics to have a feel for what's going on:

type Foo = 
  { Foo: int }

let addBaz (x: Foo) = // Foo -> {| Baz: unit; Foo: int|}
  {| x with Baz = () |}

This compiles and works as expected and does pretty much what is asked, but without the input argument being generic.

A few things to note:

  • There is no need to explicitly define the output type.
  • There is no need for a dynamic type system.

Because the compiler can infer the type definition from the code: an anonymous record with all fields from the input type Foo and the additional Baz field with unit type (this would also work if Foo already had a Baz field in which case it would be replaced).

If we make it generic we get the following compilation error

let addBaz (x: 'a) =
      {| x with Baz = () |}

FS3245: The input to a copy-and-update expression that creates an anonymous record must be either an anonymous record or a record

And that makes sense, because there is no way to say that x is a record. We could give it a class or just an int.

So, assuming we could constrain the input to be a record, could this in theory work?

Let's check a few cases by deducing types as the compiler would do for some examples:

type Foo = 
  { Foo: int }

type BazInt =
  { Baz: int }

type BazUnit =
  { Baz: unit }

type MoreFields =
  { Bar: string
    Foo: int }

let addBaz (x: BazInt) =
  {| x with Baz = () |}

let test () =
  let foo` = addBaz { Foo = 5 } // OK: addBaz monomorphizes* to `Foo -> {| Baz: unit; Foo: int |}`
  let bazInt` = addBaz { BazInt.Baz = 5 } // OK: addBaz monomorphizes* to `BazInt -> {| Baz: unit |}`
  let bazUnit` = addBaz { BazUnit.Baz = () } // OK: addBaz monomorphizes* to `BazUnit -> {| Baz: unit |}`
  let moreFields` = addBaz { MoreFields.Foo = 1; MoreFields.Bar = "" } // OK: addBaz monomorphizes* to `MoreFields -> {| Bar: string; Baz: unit; Foo: int |}`

*: I used monomorphization because it's the terminology I know from C++ templates, I'm not sure if .net uses the same terminology. But AFAIK, it doesn't matter for this discussion and the idea is the same: you just replace the generic input type 'a with the concrete type used where the function is called.

UPDATE 2:

I now get what Gus points to with the return type. The problem isn't that it can't be deduced as I showed. The problem is that somehow you need to show the generic output type. Something that currently isn't possible in F#. The return type only becomes 'complete' when it the function is called. In C++ this is perfectly fine, but in F# it isn't.

Related Questions in F#