BT

Beyond Foundations of F# - Workflows

Posted by Robert Pickering on Jan 03, 2008 |
This article builds on my previous article for InfoQ, "Beyond Foundations of F# - Active Patterns", introducing another new language feature - workflows. Chapter 11 of the introductory book to F#, "Foundations of F#" (apress, May 2007), covers language ordinate programming, a technique where program as are written using domain specific language (DSLs). These DSLs may be implemented separately using text or xml to represent the language. Often the DSL may be entirely embedded within a general purpose programming language using the data structures and other features of the language to represent the DSL. Workflows are a feature designed to build on the already strong language ordinate programming features of F#. Workflows allow you to capture a small section of code and examine its contents, giving library implementers a chance to use this as the building block for their DSLs - a technique similar to expression trees in C# 3.0. In this article will take a look under the hood and dig into how workflows work in F#.
F# is a statically typed functional programming language that targets the .NET framework. It shares a common core language with OCaml, another popular functional programming language, and draws ideas from many other programming languages, including Haskell, Erlang, and C#. In a nutshell this means that F# is a programming language that has a nice succinct syntax that feels a bit like scripting as we are able to execute the code interactively but F# has all the type safety and performance of a compiled language. This article is not indented to be an introduction to F# as there are already many resources on the web intended to make learning F# easy. See the side bar "F# Resources" in my previous article on F#.

Creating a Workflow

Workflows are composed of two parts; the code the users defines which makes up the instances of the workflow and the library component that defines what the workflow does. Let's take a look at very simple example of a workflow:

Here we see a "script" workflow is bound to the identifier "num". The workflow is made up of an F# expression surrounded by curly braces and prefixed by the identifier "script" which lets one know what the workflow actually does. Now let's take a look at the library infrastructure required to make this script work:

This is quite a bit of code. Let's break it down piece by piece and look at how each of these pieces works with our original script. The first type definition defines the type for our script. In this case a script is a function that will yield a value. The script "num" we defined earlier has the type Script because it is a function that when executed yields an integer value (in this case the integer 42). Next we define a function "runScript" used to execute a script and is also used in the definition of our "delay" which is used to delay the construction of the script until just before it is executed (this is so any side effect the script contains will be executed as the script is constructed and executed).

The next major part of the library components is the ScriptBuilder class. This defines the methods that will be used handle the various expressions that make up our script. The various parts of the expressions will be handed to the methods as values. Here the "Return" and "Let" methods handle the let and return bindings in the script. The more interesting of these is the let binding. In the let binding the "printfn" function is used to print out the value of the parameter - meaning that as the script executes the values within the let bindings are printed to the console. The idea being that this is a very useful aid for debugging. Finally, the last item shows instancing of the ScriptBuilder class. It is in this instance "script" is used within the "num" workflow. So when "num" script is executed:

runScript num

the following is output to the console:

2
21
val it : int = 42

The first two values are the values of the let bindings being printed out and the final values F# interactive automatically printed out the function result along with its type.

In workflows it is not only possible to handle normal F# expressions such as let, as shown earlier, but also new expressions such as "let!" (pronounced let bang) and "yield" along with several others. This provides the real power of workflows, allowing library implementers to take these keywords and give new meanings to them.

Now we will extend the example to include the "let!" binding expressions too. Supposing that we do not want every let binding to be printed to the console, we may just want key bindings to be printed out, implementing the "let!" provides a convenient way to allow programmers to choose which bindings to print. This is implemented by the addition of the following method to the "ScriptBuilder" class, to handle the let! binding:

We also need to make a small alteration to the "Let", to remove printing to the console:

So now when we define and run a script we can use the let! binding to print certain let bindings to the console:

Now only 21 will be printed to the console:

21
val it : int = 42

How does the F# compiler do the translation between the workflow and the final expression executed? This process is called "de-sugaring" and will dig into how it works in the next section.

Understanding De-sugaring

The expressions in a workflow are transformed into a data structure that uses "continuation passing style", this process is known as de-sugaring, as the workflow is really just "syntactic sugar".

A good place start understanding this is by coming back to our simple example:

is translated into:

The advantage of not having to type out this structure by hand is obvious. The "sugared" version is shorter and much easier to understand and typing out the non-sugared version would be both difficult and error prone. The advantages of having an expression in this form, rather than just an ordinary expression, are slightly more subtle. The important point to notice is the Bind and Let methods being called. These methods are called with the parameter to the let binding as the first parameter and a continuation - a function waiting for the value - as the second. It is this that allows us to do the trick of printing out the parameter "p" and then passing it to the "rest" function to continue the computation, seen here in the definition of the "Bind" method:

printfn "%A" p
rest p

This is the very heart of workflows in F#. The continuation style passing allows developers to insert extra actions when a value is being bound. In our example these actions are somewhat trivial, but these actions can be as involved as waiting for an asynchronous computation to complete and then continuing with the action - as we will see in the next article in this series which looks at asynchronous workflows.

Sequence Workflows

So far our little debug script may have looked like quite an unimpressive use of workflows. Let's now dig into a more realistic example using sequence workflows. Sequence workflows are a type of workflow that is available as part of the F# base class libraries. Sequence expressions are useful to create collections. For example, the following sequence expression creates a list of the first three square numbers and there square roots:

One of the most useful features of the sequence expressions are their use of the yield keyword, which is used to return a value that is part of the collection then carry on the computation. For example, the following code reads a text file yielding each line to produce a sequence of the lines of the text file:

It is also possible to use the yield!, (pronounced yield bang), keyword in sequence expressions. This allows you to add a sequence of elements to the collection and so it is often used in recursive sequence expressions. The following shows how to use a recursive sequence expression to flatten a tree of WPF controls into a sequence. This is very useful if you want to apply some operation to every item in the control tree:

We see that on the first line of the sequence expression we yield the first item we are given into the collection. Then, in the second half of the sequence expression we use a for loop to enumerate each of the control's child and recursively call "treetoList" on them. The sequence that this returns is then placed into the collection using the yield! keyword, thus flattening the tree to a list.

To see this sequence expression in action you need to use the following code to create a WPF window:

Further Reading

Workflows are covered in detail in chapter 9 of "Expert F#" by Don Syme, Adam Granicz, and Antonio Cisternino.

Conclusion

Workflows are a powerful technique that allows library designers to create libraries that have a huge amount of flexibility. Not all F# programmers will use workflows to create their own DSLs, but it is highly likely that F# programmer will use libraries written using workflows at some point, the sequence workflow are too useful to miss out on. In the next article in this series well take a look at asynchronous workflows, another innovative use of workflows that simplifies using the .NET asynchronous programming model.

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Succinct Explanation by Joe Mariadassou

I read Chap.9 of "Expert F#." This article crystallized the ideas.

Is it true that the only difference between yield and yield! is that yield! is recursive?

Thanks for making it available free of cost.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

1 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT