State in computation expressions


This is the English version of the sixth post of the F# Advent Calendar 2014 in Japanese. The original Japanese version is available here.

Computation expressions are generally regarded as the syntax for monads. But it is another important viewpoint that they are used as domain specific languages (DSLs). By using CustomOperationAttribute you can add keywords into the computation expression framework so that you can construct computation easily and safely.

The code below shows a simple example builder using the custom operation.

type CustomBuilder () =
   member __.Yield (x) = x
   [<CustomOperation("Op")>]
   member __.CustomOperation (x, arg) = arg
let custom = CustomBuilder ()
 
custom {
   Op "foo"
   Op "bar"
}

The language spec tells that the last expression is translated as the following:

custom.CustomOperation (
   custom.CustomOperation (
      custom.Yield ( () ),
      "foo"
   ),
   "bar"
)

Apart from that the computation first yields the unit value, you could find that the CustomOperations in the computation took two arguments: the first is the result of the precedent result, and the second is the argument given in the custom workflow. When simply saying that, the expression becomes a function that takes some parameter after taking the precedent result. Namely, computation expressions can have functions that take the preceding state and return the succeeding state. This means that computation expressions can behave as states.

A concrete situation: states that someone faces to one of north, east, south, and west.

type Direction = North | East | South | West

Facing to any direction, turning to right, and turning to left are actions changing direction. So you can describe the actions in the manner of computation epxpressions.

type DirectionBuilder () =
   // The first direction is north
   member __.Yield (_) = North
   [<CustomOperation("Turn")>]
   member __.Turn (_, direction) = direction
   [<CustomOperation("TurnLeft")>]
   member __.RotateLeft (direction) =
      match direction with
         | North -> West
         | East -> North
         | South -> East
         | West -> South
   [<CustomOperation("TurnRight")>]
   member __.RotateRight (direction) =
      match direction with
         | North -> East
         | East -> South
         | South -> West
         | West -> North
let direction = DirectionBuilder ()

The first state (north here) is given by the Yield method. For example, actions turning right, turning to south, turning to left, turning to left, and then turning right can be expressed as the following:

direction {
   TurnRight
   Turn South
   TurnLeft
   TurnLeft
   TurnRight
}
|> printfn "%A"  // East

This is just one perspective of the computation expressions. I hope this help for someone's understanding.