Thursday 25 June 2015

Snippet: Searching web pages.

The idea's from Professional F# 2.0, Chapter 17 (by Ted Neward, Aaron C. Erickson, Talbott Crowell, Richard Minerich). Because there are 4 of them I'll call them the fgang of four, or FoF. When searching a dropdown list via a form, a web page form may return a value or nothing at all. F# handles nothing at all with None. Suppose our database data contained:

type VacationLocation =
    { Name: string; Pop: int; Density: int; Nightlife: int }
let destinations =
    [ { Name = "New York"; Pop = 9000000; Density = 27000; Nightlife = 9 }
      { Name = "Munich"; Pop = 1300000; Density = 4300; Nightlife = 7 }
      { Name = "Tokyo"; Pop = 13000000; Density = 15000; Nightlife = 3 }
      { Name = "Rome"; Pop = 2700000; Density = 5500; Nightlife = 5 } ]

Our web page filters user selections by city name, population, population density, and night life. A user will often ignore a selection. So each selection will be an option. A first iteration of F# can handle it likewise, using the identity function id:

let getVacationPipeline nightlifeMin sizeMin densityMax searchName =
    match nightlifeMin with
    | Some(n) -> List.filter (fun x -> x.Nightlife >= n)
    | None -> id
    >> match sizeMin with
       | Some(s) -> List.filter (fun x -> x.Pop / x.Density >= s)
       | None -> id
    >> match densityMax with
       | Some(d) -> List.filter (fun x -> x.Density <= d)
       | None -> id
    >> match searchName with
       | Some(sn) -> List.filter (fun x -> x.Name.Contains(sn))
       | None -> id

This function is applied to the data like so:

let applyVacationPipeline data filterPipeline =
    data
    |> filterPipeline
    |> List.map (fun x -> x.Name)

with filterPipeline a general function to be applied for the where clause. The last line is the select clause.

let myPipeline = getVacationPipeline (Some 5) (Some 200) (Some 8000) None
applyVacationPipeline destinations myPipeline

This match ... with ... some ... None -> id is repetitive, so we abstract it out as a getFilter function. It becomes:

let getVacationPipeline2 nightlifeMin sizeMin densityMax searchName =
    let getFilter filter some =
        match some with
        | Some(v) -> List.filter (filter v)
        | None -> id
    getFilter (fun nlMax x -> x.Nightlife >= nlMax) nightlifeMin
    >> getFilter (fun sMax x -> x.Pop / x.Density >= sMax) sizeMin
    >> getFilter (fun dMin x -> x.Density < dMin) densityMax
    >> getFilter (fun sName x -> x.Name.Contains(sName)) searchName

We can invoke it like so:

let myPipeline2 = getVacationPipeline2 (Some 5) (Some 200) (Some 8000) None
applyVacationPipeline destinations myPipeline2

The FoF is more detailed, beginning with an imperative version (??). I suspect every C# programmer gave up such imperative code long ago for Linq.

F# automatically handles missing data. Because we can compose our queries, each may be simply tested too.