Tuesday, December 27, 2016

Simplest F# Asynchronous Workflow Sample

Can't nobody tell me F# async is easy; it seems hard to me. I wanted to put together the simplest sample (but different from all the samples I've seen) so I could get a better understanding. Maybe thistle help someone else. I'm searching a file line by line to see if I can find a match. In this case there are only 4 lines in the file so I'm splitting the file into 2 chunks.  Each... async... thing (after watching a long video on C# async lately and ending up hopelessly confused about when actual new threads are created I've given up guessing) gets 2 lines. In this sample, it's pretty handy how it stops looking after finding a match.

open System.IO
open System

// So we don't just print "null" to console when not found.
let OptionToStr o =
  match o with
  | None -> "not found"
  | Some(o) -> o

// Because our line gets wrapped into an option twice (once in Seq.tryPick and again in Seq.tryFind).
let UnwrapOption o =
  match o with
  | None -> None
  | Some(o) -> o

// Some(matching line) if the current chunk contains our id
let ChunkContains (id:string) (chunk:string seq) =
  |> Seq.tryPick ( fun line -> 
                   // Notice how it doesn't process all lines when it finds a match.
                   printfn "Current: %s" line
                   if line.Contains id then Some(line) else None )

// Skip n lines and call our search method for the current chunk
let ProcessFileChunkAsync (lines:string seq) skip (func: string seq -> string option) =
  async {         // Note: async is cranky.  You have to format the curly braces a certain way or compiler will complain with strange error.
    return lines  // return keyword required.
    |> Seq.skip skip
    |> Seq.truncate 2
    |> func       // 2nd parameter applied to previous partial application of ChunkContains below.

let main argv = 
  let lines = File.ReadAllLines @"c:\temp\4LinesOfTrash.txt"  // one of the lines is "hi mom"
  seq { 0 .. 1 }
  |> Seq.map ( fun i -> let chunkFun = ChunkContains "hi"   // find the line with "hi"; partial func application
                        ProcessFileChunkAsync lines (i * 2) chunkFun
  |> Async.Parallel          // Fork
  |> Async.RunSynchronously  // Join: Take the async results and wait for them to complete.
  |> Seq.tryFind ( fun item -> Option.isSome item )
  |> UnwrapOption
  |> OptionToStr
  |> printfn "Result: %A"
  |> Console.ReadLine
  |> ignore