Monday, January 17, 2011

Remote Control v0.1 Alpha

The remote control package has finally reached an alpha stage today. The library includes ways for a server program to give client programs a way to change its behavior on the fly, or to run arbitrary code on command, and receive values from those arbitrary commands.

It all started here, which brought us there. The last post in the series provided a way to inject a program's context into potentially dynamic portions of the server application. I have since refactored the library to make the client side interaction much easier.

I thought it would be easier for me to describe the library calls and use cases with code samples.

Dynamic

The dynamic call has been refactored into something that makes more sense than the previous way.

// Back to the old way, sweet!
dynamic('code) {
  println("Potentially dynamic code, without context preservation")
}

// With Context
val name = "Charlie Murphy"
dynamic('code, Context("name" -> name)) {
  println("My name is %s" format(name)
}

// Shorthand
dynamic('code, name) {
  println("My name is %s" format(name)
}

I have removed the need to pass a function that takes in a Context. This is redundant. What ever is inside the dynamic portions will know the context. I've provided a shorthand which takes in a varargs of Any.

Update

The update syntax got the most improvements.

update('code) { ctx =>
  println("There is no context, so I will speak in riddles")
}

// The update pulling values from the context
// with the key
update('code) { ctx =>
  val name: String = ctx("name")
  println("%s and I" format(name))
}

// The update applying a partial function 
// and uses Scala extractors to pull values
update('code) {
  case Context(name: String) => 
    println("Your name is not %s" format(name.reverse))
}

Reset

I've given the client the ability to reset the dynamic portion to its default state. A simple call to reset will do it.

// Back to "My name is Charlie Murphy"
reset('code)

Prepend and Append

There may be times a client doesn't want to change the dynamic portion, only add something directly before or after it. That's where prepend and append come in handy. For example:

// Takes a Context like update
prepend('code) { ctx =>
  println("Hey!")
}

append('code) { ctx =>
  println("What's yours?")
}

// When dynamic is called it will now print
// Hey!
// My name is Charlie Murphy
// What's yours?

Remote Control

That's it as far as changing a server's behavior from a client program. The next part of this post is show to how control a server from a client program. The main difference between changing behavior and controlling a server, is what the client can expect from a command. If you are still confused, think of it this way:

If you worked in the fast food industry, and your manager told you until further notice to inform customers who order a specific menu item, "We don't serve your kind here." Star Wars references aside, you, the application, changed your original behavior from serving everyone to rejecting those who order a specific item on the menu. Let's change gears now.

Your manager is thirsty, and yells, "Hey, Mikie, pass me a cold one!" He was looking at you when he spoke, so even though your name isn't Mikie, you do what your told immediately.

That is the key difference between behavioral modifications and commands.

Any server application can handle remote commands by inheriting from the RemoteControlled trait. An easier way to see this is through the scala irc robot.

import remote.RemoteControlled

class Scalabot extends Pircbot with RemoteControlled {
  override def id = 'scalabot
   // The rest is the same
  ....

To interface with the robot remotely, the client package would use the Controller object to connect and sent signals, like so:

import control.Controller._

// A remote Controller will provide itself as default context
connect('scalabot) signal {
  case Context(self: Scalabot) =>
   self.joinChannel("#scala") 
}

I provided a more complex example, to show how to retrieve data from the remote controller. A client can receive data in two ways:

  • through the signal method
  • through the signalFuture method

The signal method will try to return a response immediately, if your signal returns anything at all. The signalFuture method will immediately return a function containing the result. (This is quite literally the future. It's a function that will apply itself until the value is there). Here's the example of a client program that wants exported data from the ircbot.

import ircbot.ScalaBot
import context._
import control.Controller.connect
import signals._

object Main extends Application {
  // Computation signal takes in an optional description
  // used for logging purposes
  val compSig = Computation({
    case Context(self: ScalaBot) =>
      // Dump db as csv
      val results = self.db.query("SELECT * FROM chats")(_.executeQuery())

      results.map(_.values.mkString(","))
  } /*, "Running function which returns csv data" */)

  connect('scalabot) signal(compSig) match {
    case Some(Response(csv)) =>
      val writer = new java.io.FileWriter("chats.csv")
      csv.asInstanceOf[List[String]].map(_ + "\n").foreach(writer.write)
      writer.close()
    case _ => println("This means we did not get a response back")
  }
}

I ran the client package after chatting a few times, and the results was a chats.csv on my local machine.

1295284140634,schmee,Testing my chats
1295284147310,schmee,Doing some more chats
1295284161032,schmee,philbo is my robot

All in all, it works. I have a few more features I want to add to it, but I'm happy with it so far.

-- Philip Cali

No comments:

Post a Comment