Partager via


Step 1: Creating a Chat Room Agent

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 creates a thread-safe agent that stores messages in a chat room. The agent can add new messages and return the contents of the chat room.

This topic contains the following sections.

  • Creating a Simple Agent
  • Implementing the Chat Room Agent
  • Testing the Chat Room Interactively
  • 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.

Creating a Simple Agent

This article starts with an introduction showing how to create a simple agent that prints a received name. An agent is an object that executes an asynchronous computation and processes messages from other application components. In the current version of F#, agents are represented by a type named MailboxProcessor<'T>. This documentation uses a convenient type alias Agent<'T>.

An agent that prints all of the received messages can be written using a while loop inside an asynchronous workflow. The loop repeatedly calls the Receive member of the agent to get the next message:

type Agent<'T> = MailboxProcessor<'T>

let agent = Agent.Start(fun agent -> async {
  while true do
    let! msg = agent.Receive()
    printfn "Hello %s!" msg })

An agent is created using the Agent.Start method. The argument of the method is a function that creates an asynchronous workflow running inside the agent. The function gets the agent as an argument, so it can use the agent value to call the Receive member. Receiving a message is an asynchronous operation that may take a long time to complete (i.e., until someone sends a message to the agent), so it is called using the let! syntax. Once the asynchronous workflow resumes, the agent says "Hello" to the received name.

An agent has a type parameter that specifies the type of messages it can receive. As you can see by entering the code to F# Interactive, the type of the above agent is Agent<string>. F# inferred the type from the way the msg value is used. The value is given to the printfn function that expects a string. The next snippet tests the agent by sending it a message using the Post member:

> agent.Post("Kachna");;
val it : unit = ()
Hello Kachna!

The Post method takes a message and sends it to the agent. It doesn't wait until the message is processed, so it may return before the Hello text is printed. There are other methods that can be used when a response from the agent is required.

There are three things that are essential for understanding F# agents:

  • Messages are queued. When sending a message to an agent, the agent stores it in an internal queue until it is processed. This means that messages cannot be lost. However, if an agent receives messages faster than it can process them, the buffer size will continue to grow. (Additional resources at the end of the article offer some solutions.)

  • An agent runs on a single logical thread. The body of the agent is executed exactly once, so there is no hidden parallelization inside the agent. The next message is obtained only when the agent asks for it using Receive. However, the agent doesn't block an entire physical thread because it releases the thread while waiting.

  • Agents are cheap. The result of the fact that agents do not block threads while waiting is that they are very cheap to create. A program can create thousands of agents running on only tens of threads because a thread is used only when the agent processes messages.

The rest of the tutorial uses only a small number of agents, so it doesn't get a large degree of parallelism. However, it shows that the agent-based programming model fits the server-side programming model extremely well. The example could be extended to have multiple chat rooms and use a single agent for each chat room.

Implementing the Chat Room Agent

The first thing to consider when implementing an agent is what messages are processed by the agent. In the usual object-oriented terms, messages roughly correspond to methods or operations supported by the agent. In the previous example, the message was a string. The messages of the agents that implement multiple different operations can benefit from one of the core functional data types.

Defining the Message Type

A message is usually best represented using an F# discriminated union type. This way, a single type (for example, ChatMessage) can represent multiple different messages. As noted, the chat agent is quite simple and supports only a message for sending a new message to the chat room and a message for retrieving the contents of a chat room:

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

The first case is a message for sending a new comment to the chat room. The message carries the text to be added as a string. The second message is more interesting. It carries a value of AsyncReplyChannel<string>, which represents a reply channel. A reply channel is a mechanism for sending a reply back to the component that sent the message. For example, to print the contents of a chat room to a console, the program would send the GetContent message to the agent and then block (preferably asynchronously) until it gets a string with the entire content back.

Sending a message to an agent and waiting for the reply using a reply channel will be discussed soon. But first, the next section looks at the implementation of the agent.

Implementing the Agent

The agent created in this article keeps the content of a chat room as a list of XElement objects. This makes it easy to render the chat room as a list using the <li> HTML tag. To implement the agent in a purely functional way, the body uses a recursive function that keeps the list of elements as an argument. An alternative approach would be to use a mutable data structure, which is also safe because the body of agent is a single logical thread (and so there are no race conditions). The functional solution follows:

#r "System.Xml.Linq.dll"
open System.Xml.Linq

let chat = Agent.Start(fun agent -> 
  let rec loop elements = async {

    // Pick next message from the mailbox
    let! msg = agent.Receive()
    match msg with 
    | SendMessage text -> 
        // Add message to the list & continue
        let element = XElement(XName.Get("li"), text)
        return! loop (element :: elements)

    | GetContent reply -> 
        // Generate HTML with messages
        let html = XElement(XName.Get("ul"), elements)
        // Send it back as the reply
        reply.Reply(html.ToString())
        return! loop elements }
  loop [] )

The implementation uses the LINQ to XML libraries, so the snippet first references the appropriate assembly. In an F# Script File, this is done using the #r directive. Adding a reference in an F# project would be achieved using Add Reference in Visual Studio.

The interesting part of the sample is the body of the agent. It first declares a local function named loop. The body of the function is an asynchronous workflow that receives the next message and then uses pattern matching to handle the two possible message types:

  • When the agent receives SendMessage, the function creates a new XElement, appends it to the front of the list, keeping all messages in the chat room, and then calls itself recursively using return! The return! construct means that control is transferred to the called workflow, so the recursion doesn't keep any stack that would overflow after a number of iterations.

  • When the agent receives the GetContent message, it gets the reply channel as an argument. To build a response string, it creates a new <ul> element containing all current messages, converts it to string, and sends it to the caller using the Reply method. Then, the function continues looping with the current list of messages.

The snippet showed a simple but fully functional agent that keeps the state of a chat room. Before moving on to the next step of the tutorial, it is desirable to test the agent interactively to see how it behaves.

Testing the Chat Room Interactively

After entering the code snippet from the previous section to F# Interactive, it creates a single running agent named chat. The next step of the tutorial turns it into a reusable object to enable creating multiple chat rooms. However, now there is just a single instance running in F# Interactive, so it can be tested by sending some messages to it:

> chat.Post(SendMessage "Welcome to F# chat!");;
val it : unit = ()
> chat.Post(SendMessage "Second chat message...");;
val it : unit = ()
> chat.PostAndReply(GetContent);;
val it : string =
  "<ul>
    <li>Second chat message...</li>
    <li>Welcome to F# chat!</li>
  </ul>"

A message of type ChatMessage can be created using one of its constructors. The first two commands use the SendMessage case to post some string to the chat rooms. To do that, the snippet uses the Post method, which just sends the message to the agent without waiting for a result.

Sending the GetContent message using the Post method would reveal a problem. How to create a new AsyncReplyChannel<string>? The answer is that the channel shouldn't be explicitly created by the user. When using the PostAndReply method, the agent creates the channel and adds it to the message before sending it to the agent. The method then blocks until the message is processed (and the agent sends back the room contents as string). For testing purposes, the snippet uses a blocking method. When writing efficient server-side code, the code should avoid blocking. This will be demonstrated in the next steps of this tutorial.

Summary

This step of the tutorial began by creating a simple agent that just receives messages and prints them. Then, it implemented an agent that keeps the state of a chat room using a recursive loop. The agent processes two different messages: the first message changes the state, and the second one is used to retrieve the state of the agent.

So far, the agent was created as a let declaration that constructs a single instance of the agent. The next step shows how to turn the declaration into a .NET class. Then, it becomes possible to create multiple chat rooms just by creating new instances of the class. The next step also demonstrates how to make the type nicely accessible from C# by exposing long-running operations using Task<TResult>.

Additional Resources

This article is 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 2:

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:

Previous article: Tutorial: Creating a Chat Server Using Mailboxes

Next article: Step 2: Encapsulating Agents into Objects