F# struct member referencing 'this' results in an error
Asked Answered
I

3

9

New F# developer, long time C# developer. As an exercise in learning F#, I'm working my way through Eric Lippert's series on graph coloring translating from his C# to F#. I'm currently working on part two.

The original C# is in the blog post - here's the F# translation so far - but it doesn't compile:

type BitSet = struct
        val bits : int
        private new(b) = { bits = b }

        static member Empty = BitSet(0)

        member this.Contains (item:int) = (this.bits &&& (1<<< item)) <> 0
        member this.Add (item:int) = BitSet(this.bits ||| (1 <<< item))
        member this.Remove (item:int) = BitSet(this.bits &&& ~~~(1<<<item))
        member this.Bits = seq {
                for item in 0..31 do
                    if this.Contains(item) then
                        yield item
            }
    end

This produces the very mysterious error "error FS0406: The byref-typed variable 'this' is used in an invalid way. Byrefs cannot be captured by closures or passed to inner functions" from the definition of Bits:seq<int>.

Curiously, changing the keyword "struct" to "class" results in valid code. Coming from a C# perspective, this seems like nonsense, but I'm sure there's a valid reason behind it. The question is - how should I write the Bits function? What's the underlying F# principle that I need to understand for this to make sense?

Inceptive answered 3/1, 2017 at 21:31 Comment(0)
B
9

I think the problem is that the this reference is created as a reference to the current value of the struct, so that you can modify the struct (if you wanted and the struct was mutable).

This causes problem inside seq { .. } because this generates code inside another class and so the compiler fails to pass the reference to the "this" instance.

If you assign the this value an ordinary local variable, then the code works fine:

member this.Bits = 
    let self = this
    seq {
        for item in 0..31 do
            if self.Contains(item) then
                yield item
     }

As a matter of style - I would probably use an implicit constructor, which makes the code a bit shorter. I also prefer the Struct attribute over the explicit struct .. end syntax, but both of these are just a matter of style (and I'm sure others will have different preferences). You might find it useful just as an alternative or for comparison:

[<Struct>]
type BitSet private (bits:int) = 
    static member Empty = BitSet(0)
    member this.Contains (item:int) = (bits &&& (1<<< item)) <> 0
    member this.Add (item:int) = BitSet(bits ||| (1 <<< item))
    member this.Remove (item:int) = BitSet(bits &&& ~~~(1<<<item))
    member this.Bits = 
        let self = this
        seq {
            for item in 0..31 do
                if self.Contains(item) then
                    yield item
        }
Baalman answered 3/1, 2017 at 21:44 Comment(1)
Ah yes - thinking about it from the IL perspective, that makes perfect sense. Thanks!Inceptive
L
9

To address your point here:

This produces the very mysterious error "error FS0406: The byref-typed variable 'this' is used in an invalid way. Byrefs cannot be captured by closures or passed to inner functions" from the definition of Bits:seq. Curiously, changing the keyword "struct" to "class" results in valid code. Coming from a C# perspective, this seems like nonsense

This should not seem like nonsense from a C# perspective. Here:

struct Mutable
{
  public int x;
  public void DoIt()
  {
    Action a = () => {
      this.x = 123;
    };
  }
}

You'll get the same error in that program, along with the helpful advice that you could capture the "this" by value by copying it to a local.

This is a consequence of three facts: first that this in a struct S is of type ref S, not S, second, that variables, not values, are captured, and third, that the .NET type system does not allow storage of ref variables on the long term storage pool, aka the GC'd heap. Refs can only go on the short term storage pool: stack or registers.

Those three facts together imply that you can't store this in a struct in any way that could survive longer than the activation, but that is precisely what we need when creating a delegate; the delegate will be on the long term pool.

Leaf answered 4/1, 2017 at 7:11 Comment(1)
Yep. I'm just not seeing all the hidden lambdas in F# yet, nor appreciating all the points where C# is going out of its way to hide some of these details and just "make it work". Yet another example of why learning language B will probably teach you things you didn't know about language A. Thanks for the feedback!Inceptive
S
3

this means something different for struct vs class since they are represented differently in memory. C# is glossing over the details and making it work while F# is deciding you need to handle the difference yourself.

The correct solution is to cache the values you care about in local variables to avoid trying to use this in the seq block. You can either cache the whole object as Tomas shows or just cache the bits value and inline the Contains call.

Satanic answered 3/1, 2017 at 21:46 Comment(2)
Given that C# generates the same error (see Eric Lippert's answer), I'm not sure it's fair to say that C# is glossing over the details.Lupulin
@Brian: C# iterator syntax works which is what the OP linked to and what I was referring to. I don't know the exact method used but it appears to work or else the linked code would not compile. C# could be taking a copy but it is doing something to allow the iterator to work.Satanic

© 2022 - 2024 — McMap. All rights reserved.