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