testingf#metaprogramminglanguage-featuresfscheck

Does F# have a language construct to access the lexical scope (like python locals()/globals())


When writing tests in F# I am trying to generate useful messages about the state that caused errors. In python I would include all the locals(), so they are easily accessible in the test trace.

Is there a similar construct in F#?

I have been searching the web and the excellent fsharpforfunandprofit site, as well as gone through the list of reserved keywords.

Here is some code I would like to be able to do.

[<Property>]
let some_test x =
    let example_inner_helper y =
        let z = y + x
        // Example of the meta-construction i am looking for
        let scope = {| 
            local = {| y = y; z = z |}; 
            capture = {| 
                            local = {| x = x |};
                            capture = {| (* ... *) |};
            |};
        |}
        x = z |@ sprintf "require %A=%A (%A)" x y scope
    example_inner_helper (x+x)

Which would produce the very useful output

 Tests.some_test
   Source: Tests.fs line 105
   Duration: 51 ms

  Message: 
    FsCheck.Xunit.PropertyFailedException : 
    Falsifiable, after 1 test (1 shrink) (StdGen (387696160,296644521)):
    Label of failing property: require 1=2 ({ capture = { capture = {}
                  local = { x = 1 } }
      local = { y = 2
                z = 3 } })
    Original:
    -1
    Shrunk:
    1

However, I am having to explicitly capture the information that a construct like "scope" could automatically provide. Resulting in ugly, error prone stuff like

let ``firstAscendingLastDescendingPartE Prop`` (a: Extent<int>) b =
    let validateT ea eb =
        let checkXbeforeY ex ey headx restx =
            match restx with 
                | ValueNone -> // No rest
                    (ex = headx) |@ sprintf "no rest: ex = headx (%A = %A)" ex headx 
                    .&. ((intersectE ex ey) = ValueNone) |@ sprintf "no rest: ex ∩ ey = ∅ (%A ∩ %A)" ex ey
                | ValueSome rex -> // Some rest -> 
                    // headx and restx combines to ex
                    ((expandE headx rex) = ex) |@ sprintf "rest: headx+rex = ex (%A...%A)" headx rex 
                    .&. (exactlyTouchesE headx rex) |@ sprintf "rest: headx ends where rex starts (ex=%A ey=%A headx=%A restx=%A)" ex ey headx restx
                    .&. (exactlyTouchesE headx ey) |@ sprintf "rest: headx ends where ey starts (ex=%A ey=%A headx=%A restx=%A)" ex ey headx restx
....

(Of course, I don't care about the actual type of the "scope" construct)

Regarding unquote

I already looked at unquote which is quite cute and looks about right. But it limits the code quite a bit:

I have code that looks a bit like:

[<Struct>]
type X<'T when 'T: comparison and 'T: equality> =
    val public first: 'T
    val public last: 'T
    new (first: 'T, last: 'T) = {
        first = 
            (if last <= first then 
                invalidOp (sprintf "first < last required, (%A) is not < (%A)" first last) 
             else first)
        last = last;
    }
    // ..

So I have issues with tests using the two constructs below (causing the above errors).

let quote_limits () = 
    let x first last = X(first, last)
    let validate (x: X<'a>) = x.first < x.last
    let a = X(0, 1)
    <@ a.first = 0 @> |> test
    <@ validate a @> |> test 

The first one i can workaround with functions to access the struct parts, but the limitation on generics is PITA.


Solution

  • I don't think there is a way to do this, even in principle. F# code gets compiled to .NET bytecode which is stack based and so local variables do not really (in general) exist in the compiled code. You might be able to get some globals (they are static members) or perhaps use debug information somehow, but I don't think there is a standard way of doing that.

    That said, in your specific case, F# actually has a nice option using the unquote testing framework. This lets you put test code in F# quotations and it then shows how this quotation evaluates. For example:

    let some_test x =
        let example_inner_helper y =
            <@ 
              let z = y + x
              x = z 
            @> |> test
        example_inner_helper (x+x)
    
    some_test 10
    

    Here, I defined the local variable z inside the quotation. When you run the test, unquote will print the individual evaluation steps and you can see the value of z there:

    Test failed:
    
    let z = y + x in x = z
    let z = 20 + 10 in x = z
    let z = 30 in x = z
    false