Sunday, October 3, 2010

F# Computation Expressions: Basic Data Retrieval Mechanics

One of the uses often mentioned for F# computation expressions is that of simplifying the syntax of database retrieval. To investigate how the mechanics of this could work, I decided to implement a simple relational retrieval system. I tried to pare it down to the utmost basics, and I’m happy to say I got further than I expected; the sample problem has far more lines of code than does the computation expression itself!

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")]
  |> Rec.Make  
let colors = 
   (3,"Green");(4,"Red"); (5,"White")]
 |> Rec.Make 
let flags =
  |> 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."

No comments: