Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Removing imperative code from F# async workflows

I'm writing a control application for the Logitech Media Server (formerly known as Squeezebox Server).

A small part of that is discovering which servers are running on the local network. This is done by broadcasting a special UDP package to port 3483 and waiting for replies. If no server replies after a given time, (or a preferred server replies) the application should stop listening.

I have it working in C#, using the async/await features of C# 5, but I was curious to see how it would look in F#. I have the following function (more or less directly translated from C#):

let broadCast (timeout:TimeSpan) onServerDiscovered = async {
  use udp = new UdpClient ( EnableBroadcast = true )
  let endPoint = new IPEndPoint(IPAddress.Broadcast, 3483)
  let! _ = udp.SendAsync(discoveryPacket, discoveryPacket.Length, endPoint) 
           |> Async.AwaitTask

  let timeoutTask = Task.Delay(timeout)
  let finished = ref false
  while not !finished do
    let recvTask = udp.ReceiveAsync()
    let! _ = Task.WhenAny(timeoutTask, recvTask) |> Async.AwaitTask
    finished := if not recvTask.IsCompleted then true
                else let udpResult = recvTask.Result
                     let hostName = udpResult.RemoteEndPoint.Address.ToString()
                     let serverName = udpResult.Buffer |> getServerName 
                     onServerDiscovered serverName hostName 9090
  }

The discoveryPacket is a byte array containing the data to broadcast. getServerName is a function defined elsewhere, that extracts the human-readable server name from the server reply data.

So the application calls broadCast with two arguments, a timeout and a callback function that will be called when a server replies. This callback function can then decide to end listening or not, by return true or false. If no server replies, or no callback returns true, the function returns after the timeout expires.

This code works just fine, but I am vaguely bothered by the use of the imperative ref cell finished.

So here's the question: is there an idiomatic F#-y way to do this kind of thing without turning to the imperative dark side?

Update

Based on the accepted answer below (which was very nearly right), this is the full test program I ended up with:

open System
open System.Linq
open System.Text
open System.Net
open System.Net.Sockets
open System.Threading.Tasks

let discoveryPacket = 
    [| byte 'd'; 0uy; 2uy; 23uy; 0uy; 0uy; 0uy; 0uy; 
       0uy; 0uy; 0uy; 0uy; 0uy; 1uy; 2uy; 3uy; 4uy; 5uy |]

let getUTF8String data start length =
    Encoding.UTF8.GetString(data, start, length)

let getServerName data =
    data |> Seq.skip 1 
         |> Seq.takeWhile ((<) 0uy)
         |> Seq.length
         |> getUTF8String data 1


let broadCast (timeout : TimeSpan) onServerDiscovered = async {
    use udp = new UdpClient (EnableBroadcast = true)
    let endPoint = IPEndPoint (IPAddress.Broadcast, 3483)
    do! udp.SendAsync (discoveryPacket, Array.length discoveryPacket, endPoint) 
        |> Async.AwaitTask
        |> Async.Ignore

    let timeoutTask = Task.Delay timeout

    let rec loop () = async {
        let recvTask = udp.ReceiveAsync()

        do! Task.WhenAny(timeoutTask, recvTask)
            |> Async.AwaitTask
            |> Async.Ignore

        if recvTask.IsCompleted then
            let udpResult = recvTask.Result
            let hostName = udpResult.RemoteEndPoint.Address.ToString()
            let serverName = getServerName udpResult.Buffer
            if onServerDiscovered serverName hostName 9090 then
                return ()      // bailout signalled from callback
            else
                return! loop() // we should keep listening
    }

    return! loop()
    }

[<EntryPoint>]
let main argv = 
    let serverDiscovered serverName hostName hostPort  = 
        printfn "%s @ %s : %d" serverName hostName hostPort
        false

    let timeout = TimeSpan.FromSeconds(5.0)
    broadCast timeout serverDiscovered |> Async.RunSynchronously
    printfn "Done listening"
    0 // return an integer exit code
like image 607
corvuscorax Avatar asked Jan 01 '13 12:01

corvuscorax


1 Answers

You can implement this "functionally" with a recursive function which also produces an Async<'T> value (in this case, Async). This code should work -- it's based on the code you provided -- though I couldn't test it as it depends on other parts of your code.

open System
open System.Net
open System.Net.Sockets
open System.Threading.Tasks
open Microsoft.FSharp.Control

let broadCast (timeout : TimeSpan) onServerDiscovered = async {
    use udp = new UdpClient (EnableBroadcast = true)
    let endPoint = IPEndPoint (IPAddress.Broadcast, 3483)
    do! udp.SendAsync (discoveryPacket, Array.length discoveryPacket, endPoint) 
        |> Async.AwaitTask
        |> Async.Ignore

    let rec loop () =
      async {
      let timeoutTask = Task.Delay timeout
      let recvTask = udp.ReceiveAsync ()

      do! Task.WhenAny (timeoutTask, recvTask)
            |> Async.AwaitTask
            |> Async.Ignore

      if recvTask.IsCompleted then
          let udpResult = recvTask.Result
          let hostName = udpResult.RemoteEndPoint.Address.ToString()
          let serverName = getServerName udpResult.Buffer
          onServerDiscovered serverName hostName 9090
          return! loop ()
      }

    return! loop ()
    }
like image 64
Jack P. Avatar answered Sep 22 '22 11:09

Jack P.