简体   繁体   中英

F# type constraint for record type with specific property

I'm trying to create a generic function which requires of its type argument that it is a record type, and that it has a specific property. Here's a sample that generates the relevant compiler error:

let foo<'a> (a : 'a) =
    a' = { a with bar = "baz" }
    a'

Compiling this I get an error stating The record label bar is not defined .

I tried adding the following type constraint:

let foo<'a when 'a : (member Id : string)> =
    // ...

but that didn't compile either, complaining that This code is not sufficiently generic. The type variable ^a when ^a : (member get_Int : ^a -> string) could not be generalized because it would escape its scope. This code is not sufficiently generic. The type variable ^a when ^a : (member get_Int : ^a -> string) could not be generalized because it would escape its scope.

Is there a way to specify a type constraint that would let me do this properly?

I would suggest reading Tomas' answer first. Using statically resolved type constraints should generally be avoided when possible. They're a feature of the F# compiler rather than .NET so they do, to some extent, restrict the reusability of your code. That said, they are very powerful and do allow you to impose useful constraints at compile time.

The syntax for using them is also not terribly pleasant but if you remain undeterred, you could do something like this:

type Test = {Bar : string}

let inline foo (a : ^a) =
    "foo " + ((^a) : (member Bar : string) (a))

let foobar = foo {Bar = "bar"} // prints "foo bar"

Note however that you can't actually restrict the type to being a record, simply something that has a member Bar of type string . So this would also resolve:

type Test2(str : string) = member this.Bar = str

let foobar2 = foo (Test2("bar")) // prints "foo bar"

I don't think there is a way to specify this using static member constraints - static member constraints are fairly restricted and they are mainly an abstraction mechanism that is available in addition to other more usual techniques.

If I was trying to solve problem like this, I would probably consider using interfaces (this is not always the best way to do this, but without knowing more about your specific situation, it is probably a reasonable default approach):

type ISetA<'T> = 
  abstract WithA : string -> 'T

type MyRecord = 
  { A : string }
  interface ISetA<MyRecord> with
    member x.WithA(a) = { x with A = a }

When implementing the record, you need to add an interface implementation (so you need to do a bit more work than if you could do this using just static member constraints). But then, you are also explicitly saying that this is an intended use for the type...

The usage is also simpler than when using static constraints:

let setA (setA:ISetA<_>) = 
  setA.WithA "Hello"

setA { A = "Test" }

I'm not sure what your goal is.

If what you want is to read properties of a generic record see TheInnerLight's working example.

If, instead, you want to write a function that clones many types of records, then you should change your design. You can follow the approach suggested by Tomas.

In addition to all that, here's another alternative: use a nested generic record.

type Test<'a> = {Bar : string; Rest : 'a}

type A = {PropA : string}
type B = {PropB : int}

let a = {Bar = "bar"; Rest  = {PropA = "propA" }}


let foo a = {a with Bar = "foo " + a.Bar}

let foobar = foo {Bar = "bar"; Rest = {PropA = "propA"}} 
// val foobar : Test<A> = {Bar = "foo bar"; Rest = {PropA = "propA";};}

let foobar' = foo {Bar = "bar"; Rest = {PropB = 0}}
// val foobar' : Test<B> = {Bar = "foo bar"; Rest = {PropB = 0;};}

As a final note, with this design, you can move to Lenses as FyodorSoikin initially suggested.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM