Thursday, June 4, 2009

Quotes with Actors Part I

Several months ago, I got serious with the Scala Actor library, and quickly fell in love. Very little data is required to thread together your application with actors, and make you feel like a rawk star ;) This is part one of a two part series covering some assets of the scala actor library. First, we'll be looking at how to incorporate actors in code, then how to make application communication with RemoteActors.

The application we're going to make is a quote generator. Clients would send a request for a random quote, and the application will send a quote in the form of a string, formatted like so: "What was said" - Who said it from Where. With those requirements, we can come up with a couple of case classes for this simple application.

case class Request()
case class Quote(from: String, who: String, what: String)

What are case classes? In Scala, a case class definition is shorthand for this:

object Quote {
  def apply(from: String, who: String, what: String) = new Quote(from, who, what)
  def unapply(q: Quote): Option[(String, String, String)] = Some(q.from, q.who, q.what)
}
class Quote (val from: String, val who: String, val what)

Case classes are used for pattern matching, which the actor library makes heavy use of. The unapply is required for extracting, an incredible convenience for pattern matching. You can see that appending case in front of your class saves yourself a lot of typing.

If we go back to our requirement, it said: Clients would send a request for a random quote. A verb and a noun. Similar to: a browser makes a GET and displays the response. So the next step is to make a quote "server" with a running actor.

import java.util.Random
import scala.io.Source.{fromFile => open}
import scala.actors.Actor._

val quotes = { 
  (for(line <- open("resources/quotes.txt").getLines;
    val split = line.split("=")) yield((split(0), split(1), split(2)))).toList
}

val generator = actor {
    val rnd = new Random()
    loop {
      react {
       case Request() => {
         val (who, said, from) = quotes(rnd.nextInt(quotes.size))
         sender ! Quote(from, who, said)
      }
    }
  }
}

Alright let's break this up. The first thing I'm doing is getting a list of tuples from a text file that contains our quotes. We'll call that our "store" of quotes in val quotes. I imported everything from object Actor, but we're only concerned about actor, loop, react, and sender.

  • The actor method takes in a body of code, instantiates an Actor class, and starts our actor. It's not enough that our sever starts. The server is required to run as long as we need it.
  • That is where loop comes in. loop takes in a function, calls that function and repeats itself. It is a never ending loop, which is exactly what we want.
  • react takes in a function and applies it to any message the actor receives in its mailbox. This is where pattern matching is key. Our server is only concerned about quote requests. In the event that our server has received a request, it grabs a random quote from the store.
  • Think of sender being the from address on the mail. Our server says "send this quote to the sender of this request." That is done by the ! operator and the Quote case class to the right of it.

Now all we need is a client to make that request. The client code would look something like so:

generator !? Request() match {
  case Quote(from, who, what) => what + "\n" + " - " + who + " from " + from
  case _ => "No quotes! Sorry :("
}

The !? operator is like a send/receive method. It sends the generator a request and receive a Quote. In the event that it's not a quote (which it should always be), return an "error" string. All we do now is some formal wrapper of the code above, and we got a random quote generator.

package calico.quotes

import scala.io.Source.{fromFile => open}
import scala.actors.Actor._
import java.util.Random

case class Request()
case class Quote(from: String, who: String, what: String)

object QMachine {
  val quotes = { 
    (for(line <- open("resources/quotes.txt").getLines;
        val split = line.split("=")) yield((split(0), split(1), split(2)))).toList
  }

  val generator = actor {
    val rnd = new Random()
    loop {
      react {
        case Request() => {
          val (who, said, from) = quotes(rnd.nextInt(quotes.size))
          sender ! Quote(from, who, said)
        }   
      }   
    }   
  }
  def apply() = new QMachine(generator)
}

class QMachine(generator: scala.actors.Actor) {
  def quote = { 
    generator !? Request() match {
      case Quote(from, who, what) => what + "\n" + " - " + who + " from " + from
      case _ => "No quotes! Sorry :(" 
    }   
  }
}

Now we run this through sbt (Simple Build Tool; I highly recommend that you build all Scala projects with it), we can fire up the console and test it out.

scala> import calico.quotes.QMachine
import calico.quotes.QMachine

scala> val qm = QMachine()
qm: calico.quotes.QMachine = calico.quotes.QMachine@952527

scala> qm.quote
res0: java.lang.String = 
"Let me tell you something. There are weak men and strong men 
in this world. The strong men take everything and the weak men die. 
That's how the world was designed. Now I will show you how it works, weaklings!!!"
 - Luca Blight from Suikoden II

scala> qm.quote
res1: java.lang.String = 
"I've chopped hundreds, thousands of necks. I can do it with my eyes closed!"
 - Luca Blight from Suikoden II

scala>

Ah, nothing like uplifting quotes from RPG's most ruthless villain, Luca Blight. Some things to notice about our quote generator implementation through actors are:

  • The "client" knows about our "server", but our server could care less about the client. This separation is great, and largely popularised by the MVC paradigm.
  • A few lines of code, and we can make our quote server send quotes over the wire with RemoteActors.

And this is about it. Stay tuned for part two, when we make this "remote."

-- Philip

No comments:

Post a Comment