Way to created nested computation expressions

%fcomputation-expressions

In F#, I want to build a hierarchical data structure in a way with a minimum
amount of language noise. The actual problem is trying to build an RSpec
inspired framework using F#. RSpec allows the construction of tests in a nested way. For example.

describe Order do
  context "with no items" do
    it "behaves one way" do
      # ...
    end
  end

  context "with one item" do
    it "behaves another way" do
      # ...
    end
  end
end

I have something working here, but there are some
potential problems with the current solution. In particular general
setup/teardown code can become somewhat ugly.

My primary concern is to write an API that allows the user to write
tests/specifications in a way, where the language gets as little in the way as
possible.

I think that using computation expressions could allow my to create a better
API, but I am struggling with implementing nested contexts.

Currently I do have a builder that allows me to write:

let specs =
    describe "Some feature" {
        it "has some behavior" (fun () -> 
            // Test code goes here
        )
    }

But in order to have nested contexts I need to be able to write something like
this:

let specs =
    describe "Some feature" {
        describe "in some context" {
            it "has some behavior" (fun () -> 
                // Test code goes here
            )
        }
        describe "in some other context" {
            it "has some behavior" (fun () -> 
                // Test code goes here
            )
        }
    }

I have no idea how to implement the nested context. Or even if it is possible to
bend F# in a way to allow me to create such a builder.

I did make an experiment, that allowed me to write this:

let specs =
    desribe "Some feature" {
        child (describe "in some other" {
            it "has some behavior" (fun () -> 
                // Test code goes here
            )
        })
        child (describe "in some context" {
            it "has some behavior" (fun () -> 
                // Test code goes here
            )
        })
    }

But the added parenthesis and explicit builder construction is exactly what I
want to avoid in the first place.

type DescribeBuilder(name : string) =
    [<CustomOperation("it")>]
    member __.It (x, name, test : unit -> unit) =
        let t = TestContext.createTest name test
        x |> TestContext.addTest t
    [<CustomOperation("child")>]
    member __.Child(x, child :TestContext.T) =
        x |> TestContext.addChildContext child
    member __.Yield y =
        TestContext.create name
    member __.Delay (x) =
        x()
let describe name = new DescribeBuilder(name)

Best Answer

Have you tried any of the bang (!) keywords? You might be able to do it like so:

let specs =
    describe "Some feature" {
        do! describe "in some context" {
            do! it "has some behavior" (fun () -> 
                // Test code goes here
            )
        }
        do! describe "in some other context" {
            do! it "has some behavior" (fun () -> 
                // Test code goes here
            )
        }
    }

assuming your it returns something in the describe workflow.

(The above might be improper F# as I've been away from it for a while, but you should be able to do something like it.)