fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Error constructing struct record in member using 'with'

Open antrv opened this issue 6 years ago • 6 comments

The following code is not compiled:

[<Struct>]
type Person =
    { Name: string; Age: int }
    member x.WithAge age =
        { x with Age = age }

with the error FS3232: Struct members cannot return the address of fields of the struct by reference, pointing to the x inside WithAge member.

The problem is that x has type inref<Person>, but nothing prevents from constructing another record instance from it.

The known workaround is to declare another variable from x:

[<Struct>]
type Person =
    { Name: string; Age: int }
    member x.WithAge age =
        let copy = x
        { copy with Age = age }

Environment:

  • .NET Core 2.2
  • VS 2019 16.2.5

antrv avatar Sep 11 '19 14:09 antrv

This error is to be expected - no longer making x an inref would violate guarantees around not copying, so an explicit copy is needed if that is your aim. An alternative approach would be:

[<Struct>]
type Person =
    { Name: string; Age: int }
        
module Person =
    let withAge age p = { p with Age = age }

cartermp avatar Dec 19 '19 07:12 cartermp

@cartermp, you wrote:

no longer making x an inref would violate guarantees around not copying

However, I have come to understand that the with syntax always creates a copy, this is also what the docs say:

This form of the record expression is called the copy and update record expression.

Records are immutable by default; however, you can easily create modified records by using a copy and update expression

I don't understand why your module code creates a copy, but the OP's code does not. In his code, I don't see any reference, or is the 'this' pointer an implicit reference here and needs it (implicitly) to be dereferenced first by copying it locally?

For non struct records, that syntax would just copy and update the record and return a new copy.

If there are differences in behavior, perhaps we can update the docs? And maybe also the error?

abelbraaksma avatar Dec 19 '19 10:12 abelbraaksma

The 'this' pointer is an inref<Peson> from the F# type system standpoint. This was introduced in F# 4.5 to ensure certain guarantees around copying and structs, which is why this arises. I would imagine that this could be special-cased - do not emit this error when we are only doing a copy-and-update expression - but that kind of special casing also tends to be pretty fragile in nature. @dsyme?

cartermp avatar Dec 19 '19 18:12 cartermp

@cartermp, thanks for the explanation. I reread the byref docs, and it's included that a readonly struct is treated as inref<'T>, must've missed that detail.

However, since the with copy-and-update syntax only reads, then copies, then returns that copy, it doesn't seem to be any violation of the inref<'T> contract to allow this. But perhaps that should go into a language suggestion?

FS3232: Struct members cannot return the address of fields of the struct by reference

The error mentioned by the OP doesn't appear to be very specific to the situation, esp since the code doesn't show any attempt to return an address.

abelbraaksma avatar Dec 19 '19 20:12 abelbraaksma

This error surprises me, I can't immediately imagine why it's being triggered. We should have de-sugared to a TAST like this:

    member x.WithAge age =
        TOp.Recd ( args = [ x.Name;  age ] )

and I would have thought this would allow the x.Name in the checks in PostInferenceChecks.fs.

But clearly there's something awry.

dsyme avatar Jan 07 '20 16:01 dsyme

@dsyme, I suggest we reopen this? Your comment makes me believe this is a bug after all.

BTW, I just recollect that I have code like the one of the OP that just compiles as expected, perhaps the structness of this one causes the unexpected behavior? Something with boxing of the this pointer perhaps, making it inref?

abelbraaksma avatar Jan 07 '20 23:01 abelbraaksma