Hi,
"computation expressions" in F# are very similar to "monads", but that's not very useful analogy, because monads in Haskell have a different primary purpose. You already mentioned some examples - sequence comprehensions (seq { }), AttemptBuilder example (attempt { }), but there are also asynchronous workflows (async { }).

What they all share is that the code embedded in the "computation expressions" can be written in usual F# code, so the expression represents some executable code. However, all of them are not "standard" code. "Seq" because it can return multiple values on demand, "attempt" because it may fail and execute only part of the code, "async" because the rest of the code executes after asynchronous call is completed.

So, the short answer is that they allow us to write code that runs in a "non-standard" way. Of course, all the code needs to access "usual" F# code and that's why we have "let" and "let!" (or "do" and "do!"). The version with exclamation mark can be used for calling other non-standard code of the same type, while the ordinary "let" just executes ordinary F# code.

In C#, you can write two kinds of non-standard computaions. Brian already mentioned LINQ expressions, where you can use (somewhat limited) C# language for writing body of some code that manipulates with collections (that's similar to "seq { }"). However, C# 2.0 also has "iterators" and the "yield return" keyword, which is another way for writing non-standard computations for working with collections - again, you can return multiple values, so it is similar to "seq { }".

Interestingly, it is posible to use C# "yield return" to emulate "async { }" from F#. You can find more information here: [link:tomasp.net]

Hope this helps!
T.

By on 9/3/2008 4:38 PM ()

Hi Juliet,

Yes, Tomas is correct. The most useful way to think about computation expressions is that they provide a framework to give a non-standard interpretation to F# control flow contructs "let", "while", "try/finally", "try/with" etc.

There are only a handful of widely used kinds of computation expressions, in particular "seq" and "async". These are the reason that computation expressions exist in the F# design. As such, most F# programmers are unlikely to ever define a new kind of computation expression.

Thanks

Don

By on 9/4/2008 6:15 AM ()

Where can I find more information about how do workflows work? I have read Expert F# but, sincerely, I didn't understand it very well.

By on 9/6/2008 2:39 PM ()

Where can I find more information about how do workflows work? I have read Expert F# but, sincerely, I didn't understand it very well.

Definitely check out this article that Robert Pickering wrote for InfoQ.

By on 9/7/2008 4:58 AM ()

I know where you are coming from as I have both Expert F# and Foundations of F#. I have had to read the Workflow section over and over and it is still not clear.

I am trying to understand FP and I have blogged about functions and monoids - I am almost done with the monad section [[link:sdasrath.blogspot.com] It is under the Functional Programming tag.

In terms of monads, from what I understand, is that in order to do useful work in programming, you have to have side effects, such as IO. In a purely functional environment, there are no side effects but monads and workflows allow for side effects to take place. The whole idea behind workflow/monad is to take simple expressions, have a way of combining them and build even more complex expressions.

I like to think of monads as lego blocks. Individual blocks are simple and not very useful - however, by combining several of these simple blocks togther, you can make a more complex object.

OK I am not sure if there is an exact parallel of Workflows to something in C# but in terms of F#, workflows are like an extension of Sequence Expressions where you can either return a value (via the yield keyword) or return another sequence which can be used for further processing (via the yield! keyword).

I realized I have not fully answered your questions, but I hope that at least the monad thing is at least a little bit clearer. We are both battling workflows it seems (and also Language Oriented Programming in my case!)

I am sure either D. Syme or R. Pickering at the very least will shed some more light on the matter.
~sparky

By on 9/3/2008 12:03 PM ()

First, thank you for your replies, I appreciate it :)

In terms of monads, from what I understand, is that in order to do useful work in programming, you have to have side effects, such as IO. In a purely functional environment, there are no side effects but monads and workflows allow for side effects to take place.

I imagine workflows are more purposeful than that because F# is still an impure language without monads.

OK I am not sure if there is an exact parallel of Workflows to something in C# but in terms of F#, workflows are like an extension of Sequence Expressions where you can either return a value (via the yield keyword) or return another sequence which can be used for further processing (via the yield! keyword).

At least for me, I have no idea what the keywords in the workflow syntax are supposed to me, There's no obvious difference between 'let' and 'let!', except I mentally shout 'let!' everytime I read it.

I'm able to debug most code that I've never seen because the objects usually have very explicit interfaces such as:

- Enumerator<int> GetEnumerator()

- Customer GetCustomerFromID(long customerID)

- void Print(string text)

- int IndexOf(string input, string value)

- void Dispose()

- IDBReader ExecuteReader();

- t Value { get; set; }

If I see that much and read some source code, I have a fairly good idea of what to expect, I'm usually capable of finding errors in code with that much information alone.

Workflows on the other hand have the following interface:

- member Bind : M<'a> * ('a -> M<'b>) -> M<'b>

- member Return : 'a -> M<'a>

- member Let : 'a * ('a -> M<'b>) -> M<'b> .

- member Delay : (unit -> M<'a>) -> M<'a>

I understand that the workflow syntax is syntactic sugar to simplify the somewhat daunting interface members above. But still, of the workflow examples I've seen so far, I can't figure out how any of their let, let!, yield, yield!, return, return!, do, do! functions differ from one another without being intimately familiar with their underlying implementation, and I can't guess how to use any of the workflow objects from their interface alone.

By on 9/3/2008 3:09 PM ()

When trying to first comprehend a particular instance of a workflow/monad, I think it's a better strategy to first get your head around the associated data type 'M'. So if you're working with 'sequence expressions', you need to understand IEnumerable<'a>. If you're working with 'async workflows', you need to grok the Async<'a> type. If you're working with parsers, you need to be familiar with Parser<'a>... etc.

Looking at the definitions of Bind/Return/etc is not always so helpful. Client code examples are often more illuminating, at least for a first pass.

Most of the various F# workflow keywords have both '!' and non-'!' versions (by the way, I typically pronounce "let!" as "let-bang"), and the '!' version uses a monadic type argument (M<'a>) whereas the non-'!' version does not ('a). From a type-system point of view, in general, each monad has some way to take an instance of the monad (of type M<'a>) and extract a value (of type 'a), this is called Bind() and the syntax sugar is "let!". Similarly each monad has a way to take a normal value (of type 'a) and lift it back into the monad (make an M<'a>), this is Return() (keyword "return"). Conversely, "let" (without the '!') usually has the same meaning whether inside or outside a workflow (the left and right hand sides both have type 'a), and "return!" is a no-op (the argument is already an M<'a>, there is no lifting to be done). The transforms described near the end of this blog entry

[link:blogs.msdn.com]

(though a little out-of-date) are one of the easiest ways to step through the desugaring to grok the mechanics of it.

The behavior of these methods depends on the monad. So for IEnumerable, Bind() means 'for each value in the sequence', so code like "let! x = someSeq" will bind x (type 'a) to each element of the sequence (type IEnumerable<'a>). Return() creates a singleton sequence, e.g. transforming an int like '3' into a one-element sequence containing just the value 3. For Async, Bind() means 'run the computation and get the result', so code like "let! x = someAsync" will run the async computation on the right-hand-side (type Async<'a>) and bind the result to x (type 'a). Return() just wraps a value into a computation that will yield that value when run, so "return 2" in an async workflow results in an Async<int> that will yield 2 when run. For each monad, these two functions do something that's clever and useful to a particular domain. Most of the other operations then just make the rest of the language syntax (loops, try-finally, etc etc) appear to work reasonably in the context of that monad, but Bind() and Return() often capture the essence of the implementation. However even these functions are kind of subordinate to the data type itself (there are lots of other interesting operations you can do on IEnumerable<'a> or Async<'a> other than let! them inside a workflow - the workflow syntax just makes it easy to do a bunch of stuff associated with that data type in a way that naturally composes all the operations together to yield yet another result of type M<'a> for a given monad M, sparing you a lot of the plumbing details).

That felt rather long-winded, hope it's at least somewhat helpful.

By on 9/3/2008 4:32 PM ()

I will only partly answer the question (hopefully the 'easier' part)...

1) "LINQ" is the best fill-in-the-blank, though it is a bit misleading because LINQ in C# was designed with a very targeted domain in mind (querying _sequences_ using monadic comprehensions), whereas the F# computation expressions are a bit more general.

1.1) This pretty much translates to 'what are monads good for', and the answer, briefly, is that they can be applied in many disparate and apparently unrelated domains, which makes it very hard to sum up what monads actually are (because what they all have in common is very abstract; hard to see commonality via disparate examples). I hope to blog more about this over the coming months, but I find it very hard to write good understandable prose on the topic.

2) There is 'magic'; it is very much the same kind of magic as in C# LINQ and C# enumerators. In LINQ in C#, if you happen to add the right methods (e.g. Select, SelectMany, ...) with the right signatures to a data type, the language recognizes it and some new keywords 'light up' for that data type. The exact same thing happens with F# computation expressions, you just need the right 'structural fit' (but don't need inheritance) with various methods (Bind, Return, ...) and the computation expression syntax 'lights up' for a new type. (A similar kind of thing happens for enumerables, any type with the right signature GetEnumerator() can be used in a foreach, even if the object doesn't implement IEnumerable - this is one of the dustier corners of C# that many people aren't aware of.) Note that unfortunately I think it's hard to explain the motivation/reasoning behind these special language rules without having a good understanding of the answer to 1.1 first.

By on 9/3/2008 12:01 PM ()
IntelliFactory Offices Copyright (c) 2011-2012 IntelliFactory. All rights reserved.
Home | Products | Consulting | Trainings | Blogs | Jobs | Contact Us | Terms of Use | Privacy Policy | Cookie Policy
Built with WebSharper