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 CustomOperation
s 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.