Partager via


Step 2: Encapsulating Agents into Objects

Applies to: Functional Programming

Authors: Tomas Petricek and Jon Skeet

Referenced Image

Get this book in Print, PDF, ePub and Kindle at manning.com. Use code “MSDN37b” to save 37%.

Summary: This step takes an existing agent and encapsulates it into a .NET object. The object exposes members for synchronous and asynchronous access and a member that makes the agent usable from C#.

This topic contains the following sections.

  • Declaring the Chat Agent Type
  • Exposing the Functionality Using Members
  • Summary
  • Additional Resources
  • See Also

This article is associated with Real World Functional Programming: With Examples in F# and C# by Tomas Petricek with Jon Skeet from Manning Publications (ISBN 9781933988924, copyright Manning Publications 2009, all rights reserved). No part of these chapters may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, electrostatic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except in the case of brief quotations embodied in critical articles or reviews.

Declaring the Chat Agent Type

The previous step implemented an F# agent that represents a chat room. The agent keeps a list of messages in the room as state and supports two messages. One message adds a new comment to the chat room and the other retrieves the chat room content as HTML. This step looks at how to wrap the agent into a .NET class. There are three reasons for that:

  • Reusable chat room. An online chat application would likely have multiple chat rooms. Once the agent is wrapped in a class, new rooms can be constructed just by creating instances of the type.

  • Hiding implementation. The Agent<'T> can be used in an inappropriate way. For example, the Receive member should be used only in the body of the agent. An agent wrapped in a class can hide functionality that shouldn't be used directly.

  • Exposing a .NET component. This tutorial implements the entire application in F#. It is perfectly possible to implement a part of the application (for example, the user interface) in C#. An agent wrapped in a class is a .NET component that is easily usable from other languages.

The implementation of the agent was already explained in the Step 1: Creating a Chat Room Agent, so this article doesn't repeat the discussion. The most interesting new thing about the following listing is that it defines the agent inside an F# class type:

type internal ChatMessage = 
  | GetContent of AsyncReplyChannel<string>
  | SendMessage of string

type ChatRoom() = 
  let agent = Agent.Start(fun agent -> 
    let rec loop elements = async {
      let! msg = agent.Receive()
      match msg with 
      | SendMessage text -> 
          return! loop (XElement(XName.Get("li"), text) :: elements)
     | GetContent reply -> 
          let html = XElement(XName.Get("ul"), elements)
          reply.Reply(html.ToString())
          return! loop elements }
    loop [] )

  // TODO: Members exposing chat room functionality

The listing starts with a declaration of the ChatMessage type. The message type is used only in the implementation of the agent, so it doesn't need to be visible to the callers of the library. For this reason, the type is marked as internal.

The second type is called ChatRoom. The type is declared using implicit constructor syntax. If the chat room had any additional parameters (for example, name), they could be specified in the parentheses following the type name. The body of the type starts with private fields declared using let bindings such as agent. The initialization code is essentially a part of the constructor, so the agent will be started when a new instance of the object is created.

The type that was just declared doesn't have any members. The following section adds members that expose the necessary functionality for calling the type synchronously and asynchronously from F# as well as from C#.

Exposing the Functionality Using Members

The messages that can be sent to an agent typically correspond to the operations that the agent can perform. A more complex agent may have some messages that shouldn’t be used directly (e.g., for some internal notifications), but that's not the case with the chat room. Some operations need to be exposed as multiple members to allow both synchronous and asynchronous calls.

Adding Members for Use from F#

When sending a message to the chat room, it isn't necessary to wait until the message is processed, so the class will expose only a single nonblocking version of the member that immediately returns. The operation for getting the content from the chat room may take some time to complete because the agent first needs to process all of the previous pending messages. To support nonblocking calls, the class exposes both synchronous and asynchronous versions:

member x.SendMessage(msg) = 
    agent.Post(SendMessage msg)
member x.GetContent() = 
    agent.PostAndReply(GetContent)
member x.AsyncGetContent(?timeout) = 
    agent.PostAndAsyncReply(GetContent, ?timeout=timeout) 

The first two methods of the agent were already used in the previous step. The Post method sends a message without waiting and the PostAndReply method synchronously waits for a response.

The new method in this example is PostAndAsyncReply. The method is similar to PostAndReply but it returns an asynchronous computation that waits for a reply without blocking a thread. The snippet also adds an optional parameter timeout. When the agent doesn't reply within the specified time limit, an exception is thrown. This can be useful for handling error conditions of the agent.

The chat room agent returns the content as text, so the return type of GetContent is string. The method can be called from any F# code. However, when called from the main user-interface thread, it may cause the application to freeze. The return type of AsyncGetContent is Async<string>. This means that it can be called from other asynchronous computations using the let! construct. The following snippet shows an example:

> let room = new ChatRoom();;
val room : ChatRoom

> async { 
    while true do
      do! Async.Sleep(10000)
      let! html = room.AsyncGetContent()
      printfn "%s" html } |> Async.Start
  ;;
val it : unit = ()

> room.SendMessage("Hello world!")
  room.SendMessage("Welcome to F# chat!");;
val it : unit = ()

<ul>
  <li>Welcome to F# chat!</li>
  <li>Hello world!</li>
</ul>

The second command creates an asynchronous workflow that contains a loop that waits 10 seconds and then gets the current content of the chat room and prints it. The waiting is done asynchronously using the do! construct because the Sleep operation doesn't return any results. (The return type is Async<unit>.) Because the snippet is written as an asynchronous workflow, it can get the room content using a nonblocking call. The AsyncGetContent method returns Async<string>, so it needs to be called using the let! construct.

Once the workflow starts, the program can continue doing other work. In the snippet above, it sends two messages to the chat room using the SendMessage method. After 10 seconds, the workflow resumes executing and sends the GetContent message to the agent. When the agent replies, the workflow resumes again and prints the chat room content to the F# Interactive window.

The three members added in this section are all that is needed to use the ChatRoom type comfortably from F#. As the next section shows, the type can be also easily extended to make it usable from C# as well.

Adding Members for Use from C#

Asynchronous computations in F# are represented as type Async<T>. It is possible to use it directly from C#, but it doesn't feel very natural because the type needs to be manipulated using functions from the F# library. To provide a better interface for calling the agent from C#, one can add methods that expose asynchronous operations using the Task<T> type.

The usual naming convention in C# is to append Async to the end of the method name. This doesn't conflict with F#, where Async usually appears at the beginning of the name. The following snippet adds an overloaded asynchronous method for getting the chat room content from C#:

member x.GetContentAsync() = 
    Async.StartAsTask(agent.PostAndAsyncReply(GetContent))

member x.GetContentAsync(cancellationToken) = 
    Async.StartAsTask
     ( agent.PostAndAsyncReply(GetContent), 
       cancellationToken = cancellationToken )

Converting an F# asynchronous computation of type Async<T> to a task Task<T> can be easily done using Async.StartAsTask. The largest difference between the two types is that F# asynchronous workflows do not start automatically when the method is called. They are started later by the let! construct. On the other hand, a task created using StartAsTask starts executing immediately.

The usual C# pattern for writing asynchronous methods is to provide an option to specify CancellationToken for cancelling the operation. Optional parameters in F# are implemented in a different way than in C#, so the snippet implements the method using overloading instead.

To finish the example, the following simple C# snippet uses the ChatRoom type. It can be tested after compiling the F# code into a library and referencing it from a C# project:

// Create new chat room and send a message to it
var chat = new ChatRoom();
chat.SendMessage("Hello from C#!");
// Start getting content and print it when it's available
var task = chat.GetContentAsync();
task.ContinueWith(task => {
        Console.WriteLine(task.Result);
    });
// Wait for the user input
Console.ReadLine()

The first two lines of the snippet create a new instance of the chat room and send a message to the room. As you can see, F# members are compiled as standard methods, so the snippet calls them in the usual way. The GetContentAsync method returns a Task<string> that represents a running computation. To run some code when the computation completes, the Task Parallel Library provides the ContinueWith method. The lambda expression prints the chat room content to the console. The snippet ends with a ReadLine call to make sure that the continuation can be executed before the application terminates.

Summary

The first step of the tutorial described a simple chat room agent. This step showed how to turn that agent into a reusable .NET class. To do that, the class included members that expose the two operations of the chat room. Operations that return a result should be exposed both as synchronous and asynchronous methods so that users of the object can write nonblocking code. The snippet looked at how to expose the asynchronous version using both Async<T> that is used in F#, as well as Task<T>, which can be called from C#.

The next step starts developing a web-based user interface for the chat room application. It discusses how to use the agent-oriented programming model to create a simple HTTP web server.

Additional Resources

This article is a part of a tutorial that demonstrates how to use agents to create a web-based chat server. The best way to read the tutorial is to follow the steps in order, so it is recommended to continue by reading Step 3:

This tutorial demonstrates how to use agents to develop one particular server-side application. More information about server-side programming with agents in general and for various design patterns that involve agents can be found in the following articles:

To download the code snippets shown in this article, go to https://code.msdn.microsoft.com/Chapter-2-Concurrent-645370c3

See Also

This article is based on Real World Functional Programming: With Examples in F# and C#. Book chapters related to the content of this article are:

  • Book Chapter 13: “Asynchronous and data-driven programming” explains how asynchronous workflows work and uses them to write an interactive script that downloads a large dataset from the Internet.

  • Book Chapter 14: “Writing parallel functional programs” explains how to use the Task Parallel Library to write data-parallel and task-based parallel programs. This approach complements agent-based parallelism in F#.

  • Book Chapter 16: “Developing reactive functional programs” discusses how to write reactive user interfaces using asynchronous workflows and events. This approach is related to agents but more suitable for creating user interfaces.

The following MSDN documents are related to the topic of this article:

  • Asynchronous Workflows (F#) explains how to use F# asynchronous workflows for writing efficient computations that do not block the execution of other work.

  • Classes (F#) explains how to declare standard .NET classes with constructors and members in F#.

  • Constructors (F#) provides more details about the implicit constructor syntax and let bindings in a class that we used in the example.

  • Task Parallelism (Task Parallel Library) discusses how to use the Task<T> type from C#. We used the type to expose asynchronous operation to C# users.

Previous article: Step 1: Creating a Chat Room Agent

Next article: Step 3: Writing an Agent-Based Web Server