The test database itself consists of two record types. The first is a simple key/value pair. The second is a linking record type which consists of two keys. This keeps things simple for the test, but the method would remain the same even for more complex record structures.
The computation expression doesn’t really do much other than see that work is properly partitioned into the classic collection and decision phases. Even less work is done by the operators (from, where, and select), which don’t have any function other than documentation. My test code uses a simple database of flags and colors, where the tables are implemented as lists.
Disclaimer: this oversimplified code is designed to illustrate the basic mechanics of this type of workflow. It is *not* to be considered ideal, practical or canonical! To solve this type of problem in production code, please use the F# Linq extensions; they are consistent, composable, and standard. (And as always, the code and information here are presented "as-is" and without warranty or implied fitness of any kind; use at your own risk.)
Credit: I think I first saw this idea from a web video, but I can’t recall where (possibly Channel9?). If anyone knows and can point me to it, I’ll be happy to revise this post to give credit to the source.
// Micro relational database.
/// ID,Value table.
type Rec =
{
Key : int;
Value : string
}
static member Make (k,v) =
{ Key=k; Value=v }
/// ID to ID relational link table.
type Link =
{
KeyFrom : int;
KeyTo : int;
}
static member Make (kf,kt) =
{ KeyFrom=kf; KeyTo=kt }
/// Micro query workflow.
/// Note the austere quality of the functions.
/// The real work is done by the collect in
/// the first bind and the decision in the second.
/// The workflow just sees to it that things
/// get routed properly.
type MicroQuery () =
// This de-sugars the "from" operator.
member this.Bind (s,f) =
Seq.collect f s
// This de-sugars the "where" operator.
member this.Bind (b,f) =
if b then f() else Seq.empty
// This de-sugars the "select" operator.
member this.Return v =
Seq.singleton v
// Since all the logic is contained
// in the workflow, these are not really
// necessary except as a form of
// self-documentation.
let inline from s = s
let inline where b = b
let inline select v = v
// For test data, I define some countries and
// the colors of their flags.
// For consistency, I used the names
// and spellings used in the USA.)
// If I left out your country, consider it
// an exercise to the reader to add it!
let countries =
[(0,"Australia"); (1,"Brazil");(2,"Germany");
(3,"Italy"); (4,"Mexico");(5,"Netherlands");
(6,"South Africa");(7,"Spain"); (8,"USA")]
|> List.map Rec.Make
let colors =
[(0,"Black");(1,"Blue");(2,"Gold");
(3,"Green");(4,"Red"); (5,"White")]
|> List.map Rec.Make
let flags =
[(0,1);(0,4);(0,5);
(1,1);(1,2);(1,3);
(2,0);(2,2);(2,4);
(3,3);(3,4);(3,5);
(4,3);(4,4);(4,5);
(5,1);(5,4);(5,5);
(6,0);(6,1);(6,2);
(6,3);(6,4);(6,5);
(7,2);(5,4);
(8,1);(8,4);(8,5)]
|> List.map Link.Make
/// This query will return the colors of
/// a country's flag.
let flagColors countryIn = MicroQuery () {
let! country = from countries
do! where country.Value=countryIn
let! flag = from flags
do! where country.Key=flag.KeyFrom
let! color = from colors
do! where color.Key=flag.KeyTo
return select color.Value }
/// Test it:
let flagColorsBrazil =
flagColors "Brazil"
|> Seq.toList
printfn "Your breakpoint here."