13

Why is t.b evaluated on every call? And is there any way how to make it evaluate only once?

type test =
  { a: float }
  member x.b =
    printfn "oh no"
    x.a * 2.

let t = { a = 1. }
t.b
t.b
Oldrich Svec
  • 4,191
  • 2
  • 28
  • 54
  • It is disappointing that the F# language does not support one-time-calculated-values for immutable records. I suppose the complication is if `a` is flagged as mutable. – Wallace Kelly Nov 03 '14 at 01:43

4 Answers4

16

An alternative version of Brian's answer that will evaluate b at most once, but won't evaluate it at all if B is never used

type Test(a:float) =
    // constructor
    let b = lazy
                 printfn "oh no"
                 a * 2.
    // properties
    member this.A = a
    member this.B = b.Value
Ganesh Sittampalam
  • 28,821
  • 4
  • 79
  • 98
13

It's a property; you're basically calling the get_b() member.

If you want the effect to happen once with the constructor, you could use a class:

type Test(a:float) =
    // constructor
    let b =   // compute it once, store it in a field in the class
        printfn "oh no"
        a * 2.
    // properties
    member this.A = a
    member this.B = b
Brian
  • 117,631
  • 17
  • 236
  • 300
  • You are right, but using classes I lose things like let c = {t with a = 4.}, right? – Oldrich Svec May 20 '10 at 12:55
  • 2
    Yes, but you can write a constructor with optional parameters and get a very similar effect. – Brian May 20 '10 at 19:56
  • 1
    I do not get your idea. Imagine that I have a Record with constructor having 10 parameters like {a:float; b:float, c: float...}. Creating a new record from an old one is done as {old with c = 5}. How do I do the same with classes without rewriting all parameters in the constructor? – Oldrich Svec May 21 '10 at 10:55
4

In response to your comments in Brian's post, you can fake copy-and-update record expressions using optional/named args. For example:

type Person(?person:Person, ?name, ?age) =

    let getExplicitOrCopiedArg arg argName copy =
        match arg, person with
        | Some(value), _ -> value
        | None, Some(p) -> copy(p)
        | None, None -> nullArg argName

    let name = getExplicitOrCopiedArg name "name" (fun p -> p.Name)
    let age = getExplicitOrCopiedArg age "age" (fun p -> p.Age)

    member x.Name = name
    member x.Age = age

let bill = new Person(name = "Bill", age = 20)
let olderBill = new Person(bill, age = 25)

printfn "Name: %s, Age: %d" bill.Name bill.Age
printfn "Name: %s, Age: %d" olderBill.Name olderBill.Age
Daniel
  • 47,404
  • 11
  • 101
  • 179
2

The previous responses suggest switching to a class, instead of using a record. If you want to stay with records (for their simple syntax and immutability), you can take this approach:

type test =
    { a : float
      b : float }
    static member initialize (t: test) =
        { t with b = t.a * 2. }

This is useful if the instance of test is created by another library (like a data provider from a web service or database). With this approach, you must remember to pass any instance of test that you receive from that API through the initialize function before using it in your code.

Wallace Kelly
  • 15,565
  • 8
  • 50
  • 71
  • You can use a private constructor so you don't have to remember to call the initialize function. Scott Wlaschin has some examples here: https://gist.github.com/swlaschin/54cfff886669ccab895a – DharmaTurtle Aug 30 '19 at 18:32