1

So, I want to build a custom computation expression that would allow me to turn this -

testWorld |>
    subscribe ClickTestButtonAddress [] addBoxes |>
    addScreen testScreen TestScreenAddress |>
    setP (Some TestScreenAddress) World.optActiveScreenAddress |>
    addGroup testGroup TestGroupAddress |>
    addEntityGuiLabel (testLabelGuiEntity, testLabelGui, testLabel) TestLabelAddress |>
    addEntityGuiButton (testButtonGuiEntity, testButtonGui, testButton) TestButtonAddress |>
    addEntityActorBlock (testFloorActorEntity, testFloorActor, testFloor) TestFloorAddress |>
    (let hintRenderingPackageUse = HintRenderingPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HRPU = () }
     fun world -> { world with RenderMessages = hintRenderingPackageUse :: world.RenderMessages }) |>
    (let hintAudioPackageUse = HintAudioPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HAPU = () }
     fun world -> { world with AudioMessages = hintAudioPackageUse :: world.AudioMessages })

into something like this -

fwd {
    do! subscribe ClickTestButtonAddress [] addBoxes
    do! addScreen testScreen TestScreenAddress
    do! setP (Some TestScreenAddress) World.optActiveScreenAddress
    do! addGroup testGroup TestGroupAddress
    do! addEntityGuiLabel (testLabelGuiEntity, testLabelGui, testLabel) TestLabelAddress
    do! addEntityGuiButton (testButtonGuiEntity, testButtonGui, testButton) TestButtonAddress
    do! addEntityActorBlock (testFloorActorEntity, testFloorActor, testFloor) TestFloorAddress
    let hintRenderingPackageUse = HintRenderingPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HRPU = () }
    do! fun world -> { world with RenderMessages = hintRenderingPackageUse :: world.RenderMessages }
    let hintAudioPackageUse = HintAudioPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HAPU = () }
    do! fun world -> { world with AudioMessages = hintAudioPackageUse :: world.AudioMessages }}
    <| runFwd testWorld

Is this possible, or close to something possible? If so, what approach could be taken? Is this a monad, or something lesser?

Bryan Edds
  • 1,696
  • 12
  • 28
  • 1
    Just wondering, what would be the benefit of using computation expressions? (the pipelining notation looks fine to me...) – Tomas Petricek Oct 15 '13 at 05:50
  • Most simply, the ability to set breakpoints on each individual operation. The pipeline one is practically undebug-able, AFAICT. Truth be told, my preference would be for VS to enable putting breakpoint inside the pipeline one, but I don't see that happening. Beyond all that, I want to maybe add some custom CE operators. I'm ultimately trying to build a little 'scripting' computation expression thing at the level of the World type. – Bryan Edds Oct 15 '13 at 06:11
  • Of course, my comment presumes that there is some way to get the current world out of the debugger... Tho maybe there wouldn't be :( ... Maybe I should just go back to normal let-based code? – Bryan Edds Oct 15 '13 at 06:24
  • Looks like what you need is State monad where World is the state – Ankur Oct 15 '13 at 08:52
  • http://tech.blinemedical.com/debugging-piped-sequences-f/ seems to be somewhat useful for what you're trying to achieve – N_A Oct 15 '13 at 14:01
  • @Arkur - I tried the State monad, and it works, but it was much more verbose than what I wanted the code to be. At that point, I figured I might as well go back to a series of let expressions. :/ – Bryan Edds Oct 15 '13 at 14:02
  • @mydogisbox, I appreciate the suggestion, but that's still not user-friendly enough of a debugging experience for me. Even selectively overriding the |> operator and breaking on that is just... not there. – Bryan Edds Oct 15 '13 at 14:22

4 Answers4

6

Maybe I should just go back to doing this -

let tw_ = testWorld
let tw_ = subscribe ClickTestButtonAddress [] addBoxes tw_
let tw_ = addScreen testScreen TestScreenAddress tw_
let tw_ = setP (Some TestScreenAddress) World.optActiveScreenAddress tw_
let tw_ = addGroup testGroup TestGroupAddress tw_
let tw_ = addEntityGuiLabel (testLabelGuiEntity, testLabelGui, testLabel) TestLabelAddress tw_
let tw_ = addEntityGuiButton (testButtonGuiEntity, testButtonGui, testButton) TestButtonAddress tw_
let tw_ = addEntityActorBlock (testFloorActorEntity, testFloorActor, testFloor) TestFloorAddress tw_
let tw_ = { tw_ with RenderMessages = hintRenderingPackageUse :: tw_.RenderMessages }
{ tw_ with AudioMessages = hintAudioPackageUse :: tw_.AudioMessages }

It debugs fine (you can find all previous versions of tw_ in the Autos window and can step debug on each operation), and it's not too verbose I suppose.

Bryan Edds
  • 1,696
  • 12
  • 28
  • how could I not see this. that is exactly what I mean. monad would not serve you better here I think – nicolas Oct 15 '13 at 08:42
2

If you use the ExtCore library (available on NuGet), it provides an operator called tap which is specifically provided to help debug pipelined expressions. You use it by "tapping" your pipeline, which applies an action function (returning unit) to the value at some point in the pipeline, then the value is passed through so it keeps flowing through the pipeline as expected.

For example:

testWorld
|> subscribe ClickTestButtonAddress [] addBoxes
|> addScreen testScreen TestScreenAddress
// Check to see if the screen was added correctly
|> tap (fun world ->
    // TODO : Insert code to check if the screen was added.
    // Or, put some dummy code here so you can set a breakpoint
    // on it to inspect 'world' in the debugger.
    ())
|> setP (Some TestScreenAddress) World.optActiveScreenAddress
|> addGroup testGroup TestGroupAddress
|> addEntityGuiLabel (testLabelGuiEntity, testLabelGui, testLabel) TestLabelAddress
|> addEntityGuiButton (testButtonGuiEntity, testButtonGui, testButton) TestButtonAddress
|> addEntityActorBlock (testFloorActorEntity, testFloorActor, testFloor) TestFloorAddress
|> (let hintRenderingPackageUse = HintRenderingPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HRPU = () }
    fun world -> { world with RenderMessages = hintRenderingPackageUse :: world.RenderMessages }) 
|> (let hintAudioPackageUse = HintAudioPackageUse { FileName = "AssetGraph.xml"; PackageName = "Misc"; HAPU = () }
    fun world -> { world with AudioMessages = hintAudioPackageUse :: world.AudioMessages })

A related function, tapAssert, can be used to insert debugging assertions into your pipeline.

Jack P.
  • 11,487
  • 1
  • 29
  • 34
  • I appreciate the suggestion, but I really need to be able to insert break points without restarting the program. – Bryan Edds Oct 15 '13 at 13:58
2

The State or the Writer Monad might be useful but if you want to use the debugger forget about Monads, it's even worse. One thing you can do is redefine the pipeline operator like this:

let (|>) a b = printfn "Value is: %A" a; b a

5 
    |> ((+) 40) 
    |> string 
    |> Seq.singleton  
    |> Seq.toArray

You'll see:

Value is: 5
Value is: 45
Value is: "45"
Value is: seq ["45"]
val it : string [] = [|"45"|]

Or you can instead of printing, accumulate results in a mutable list of objects.

let mutable results = [] : obj list
let (|>) a b = 
    results <- results @ [box a] // or set a breakpoint here
    b a  
...
val mutable results : obj list = [5; 45; "45"; <seq>]

This is almost like the writer monad.

But if you definitely want to use the debugger (it's not my favorite tool) you're solution with let is fine though you have to change your code and you loose the pipe forward, In that case it might be better to add a breakpoint in the body of the (|>) function.

Gus
  • 25,839
  • 2
  • 51
  • 76
  • Heh, that's cool, but I definitely don't want to print the world :) In defense of monads, at least you can put a break point on each operation - just don't try to step debug :) – Bryan Edds Oct 15 '13 at 14:48
  • You can also set a breakpoint in the body of the redefinition of this operator. Just do it multiline, I added a comment in the last sample. – Gus Oct 15 '13 at 15:26
1

You'd need a let! to propagate the monadic state.

So I guess you have the monad already : it is the plain old let which takes the current bindings, add a new one, and pass it on subsequent computations !

nicolas
  • 9,549
  • 3
  • 39
  • 83