'Why does "let" in a F# constructor create a private member instead of a local variable

I'm learning me some F#, and I'm trying to figure out how constructors work.

I want to write a class that takes some input data, parses it, and makes the results available to the outside world via member variables. The parsing process is non-trivial, so I want to create some local variables along the way, but how can I do that without them becoming private member variables, as well? AFAICT, using let to create a private member variable is almost the same as using member private, but I don't want these temporary variables to pollute the object's namespace.

This is the best I've been able to come up with, so far:

type MyClass( inputData ) = 

    let _parsedData = 
        // simulate expensive parsing of the input data
        let work = sprintf "< %s >" inputData
        sprintf "[%s]" work

    member this.parsedData = _parsedData

    member this.dump () = 

        // this doesn't compile (as expected)
        //printfn "work = %s" work

        // I want this to *not* compile (because _parsedData is a local variable in the constructor)
        printfn "_parsedData = %s" _parsedData

[<EntryPoint>]
let main argv = 
    let obj = MyClass "hello, world!"
    printfn "obj.parsedData = %s" obj.parsedData
    obj.dump()
    0 

But _parsedData becomes a private member variable, which is not necessary since it's just a temporary working variable, and the final value is stored in this.parsedData. The SO post I linked to above suggests that variables created using let will act as local variables and be discarded, as long as they are not referenced in other members, but the act of defining this.parsedData to return _parsedData is enough to keep _parsedData alive.

I could use lazy evaluation:

let _parsedDataLazy = lazy ( 
    // simulate expensive parsing of the input data
    let work = sprintf "< %s >" inputData
    sprintf "[%s]" work
) 

member this.parsedDataLazy = _parsedDataLazy.Value

but this doesn't really help, since it still has the problem of _parsedDataLazy becoming a private member variable (although in this case, it makes sense). This approach also means keeping inputData alive until the first time parsedDataLazy is called, which may not be desirable/possible.

I also thought of using val to define the member variable, then execute the parsing code to populate it, but do bindings have to appear before any member's :-/

I just want to be able to use local variables in a constructor, to calculate a value, then store it in the object. Why does let create a private member variable, given that there's already a way of doing that?! The purpose of a constructor is to initialize the object being created, it's just a function, so I don't get why there are these special restrictions on when code can be executed, or different behaviour (e.g. if I use let to define a new variable in a member function, it doesn't get hoisted into the object as a member variable).

As an aside, if I create a private member variable in the constructor using let, like this:

let _foo = 42

then I access it like this:

let member this.printFoo () =
    printfn "_foo = %s" _foo // no "this"

But if I create it like this:

member private _foo = 42

then I access it like this:

let member this.printFoo () =
    printfn "_foo = %s" this._foo // uses "this"

This different syntax suggests that the former is creating a closure over the constructor, and keeping the _foo variable alive for the life of the object, rather than _foo actually being member of the object. Is this what's actually happening?

f#


Solution 1:[1]

To answer @konst-sh's question "why do you think it should be evaluated on every call?", I don't think it should, but that's not what I'm seeing.

My understanding is that for the code below, the 3 statements that make up parsedData are an expression, that evaluates to a string (the output of sprintf), that is stored in a member variable.

type MyClass( inputData ) =

    member this.parsedData =
        // simulate expensive parsing of the input data
        printfn "PARSE"
        let work = sprintf "< %s >" inputData
        sprintf "[%s]" work

[<EntryPoint>]
let main argv =
    let obj = MyClass "hello, world!"
    printfn "CONSTRUCTED"
    printfn "obj.parsedData = %s" obj.parsedData
    printfn "obj.parsedData = %s" obj.parsedData
    0

But when I run it, I get this:

CONSTRUCTED
PARSE
obj.parsedData = [< hello, world! >]
PARSE
obj.parsedData = [< hello, world! >]

I would expect to see this:

PARSE
CONSTRUCTED
obj.parsedData = [< hello, world! >]
obj.parsedData = [< hello, world! >]

Stepping through in VSCode also confirms that the 3 statements get executed twice. But parseData is not a function, right? For that, I would need to define it like this:

    member this.parsedData () =
        ...

It feels like I'm missing something fundamental here... :-)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 taka