Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do F# computation expressions require a builder object (rather than a class)?

F# computation expressions have the syntax:

ident { cexpr }

Where ident is the builder object (this syntax is taken from Don Syme's 2007 blog entry).

In all the examples I've seen, builder objects are singleton instances, and stateless to boot. Don gives the example of defining a builder object called attempt:

let attempt = new AttemptBuilder()

My question: Why doesn't F# just use the AttemptBuilder class directly in computation expressions? Surely the notation could be de-sugared to static method calls just as easily as instance method calls.

Using an instance value means that one could in theory instantiate multiple builder objects of the same class, presumably parameterised in some way, or even (heaven forbid) with mutable internal state. But I can't imagine how that would ever be useful.


Update: The syntax I quoted above suggests the builder must appear as a single identifier, which is misleading and probably reflects an earlier version of the language. The most recent F# 2.0 Language Specification defines the syntax as:

expr { comp-or-range-expr }

which makes it clear that any expression (that evaluates to a builder object) can be used as the first element of the construct.

like image 841
Todd Owen Avatar asked Sep 09 '12 12:09

Todd Owen


People also ask

Why is there no f13 fighter?

Since the unification of the numbering system in 1962, U.S. fighters have been designated by consecutive numbers, beginning with the F-1 Fury. F-13 was never assigned to a fighter due to triskaidekaphobia, though the designation had previously been used for a reconnaissance version of the B-29.

Why is the F-16 stick on the side?

A computer interface is required to interpret pilot inputs and move the flight controls accordingly, technology known as “fly-by-wire.” Because the F-16 is designed for high-G loading, the stick is mounted on the side of the cockpit instead of in the center to make it easier on the pilot's right arm.

Does the F-22 have a side stick?

The side-stick is used in many modern military fighter aircraft, such as the F-16 Fighting Falcon, Mitsubishi F-2, Dassault Rafale, and F-22 Raptor, and also on civil aircraft, such as the Sukhoi Superjet 100, Airbus A320 and all subsequent Airbus aircraft, including the largest passenger jet in service, the Airbus ...

Why is F-22 the best?

Thrust vectoring means the F-22 can point its engine nozzles slightly up and down for greater control and maneuverability. Very few aircraft on the planet have that capability, and skilled Raptor pilots like Gunderson can use it to tie adversaries in knots.


3 Answers

Your assumption is correct; a builder instance can be parameterized, and parameters can be subsequently used throughout the computation.

I use this pattern for building a tree of mathematical proof to a certain computation. Each conclusion is a triple of a problem name, a computation result, and a N-tree of underlying conclusions (lemmas).

Let me provide with a small example, removing a proof tree, but retaining a problem name. Let's call it annotation as it seems more suitable.

type AnnotationBuilder(name: string) =
    // Just ignore an original annotation upon binding
    member this.Bind<'T> (x, f) = x |> snd |> f
    member this.Return(a) = name, a

let annotated name = new AnnotationBuilder(name)

// Use
let ultimateAnswer = annotated "Ultimate Question of Life, the Universe, and Everything" {
    return 42
}
let result = annotated "My Favorite number" {
    // a long computation goes here
    // and you don't need to carry the annotation throughout the entire computation
    let! x = ultimateAnswer
    return x*10
}
like image 173
bytebuster Avatar answered Oct 17 '22 19:10

bytebuster


It's just a matter of flexibility. Yes, it would be simpler if the Builder classes were required to be static, but it does take some flexibility away from developers without gaining much in the process.

For example, let's say you want to create a workflow for communicating with a server. Somewhere in the code, you'll need to specify the address of that server (a Uri, an IPAddress, etc.). In which cases will you need/want to communicate with multiple servers within a single workflow? If the answer is 'none' then it makes more sense for you to create your builder object with a constructor which allows you to pass the Uri/IPAddress of the server instead of having to pass that value around continuously through various functions. Internally, your builder object might apply the value (the server's address) to each method in the workflow, creating something like (but not exactly) a Reader monad.

With instance-based builder objects, you can also use inheritance to create type hierarchies of builders with some inherited functionality. I haven't seen anyone do this in practice yet, but again -- the flexibility is there in case people need it, which you wouldn't have with statically-typed builder objects.

like image 28
Jack P. Avatar answered Oct 17 '22 20:10

Jack P.


One other alternative is to make use of single case discriminated unions as in :

type WorkFlow = WorkFlow with
    member __.Bind (m,f) = Option.bind f m
    member __.Return x = Some x

then you can directly use it like

let x = WorkFlow{ ... }
like image 36
Onur Gumus Avatar answered Oct 17 '22 18:10

Onur Gumus