Wednesday, February 11, 2009

Refactoring the twit.pl Perl script to interact with twitter.com and identi.ca

Today I'll be revisiting the twit.pl script.
The new version (0.0.2) will be a (hopefully) good practice on the use of functions (or subroutines).

Version numbering explained
A short sidenote about the version numbering that I use (x.y.z):
- I'll be updating the last number (z) when the program interface stays the same and only internal changes are made.
- The middle number (y) will be increased when a new functionality is introduced
- The first part (x) notifies the user that they have to change the way that they are using the program (command format changed for example).

In version 0.0.1 of the code, you'll have noticed that we repeat almost the same code at the end for twitter.com and identi.ca updates:
if ($TwitterUse) {
my $twitter = Net::Twitter->new(username => $TwitUser, password => $TwitPass);
if ($twitter->update($Status)) {
say "twitter: OK";
}
else {
say "twitter: FAIL";
}
}
if ($IdenticaUse) {
my $identica = Net::Twitter->new(identica => 1, username => $IdenticaUser, password => $IdenticaPass);
if ($identica->update($Status)) {
say "Identica: OK";
}
else {
say "Identica: FAIL";
}
}
twit.pl v0.0.2
I'll try to refactor the code by using functions. In the end, I want the result to be something like:
say SendMessage("Twitter", $Status, $TwitUser, $TwitPass) if ($TwitterUse);
say SendMessage("Identica", $Status, $IdenticaUser, $IdenticaPass) if ($IdenticaUse);
SendMessage is a function that will have for:
- input = "twitter" or "identica" string, status string (your message), login, password
- output = string with update status or error message

Notice how Perl lets you add the if(..) statement at the end of the line when there is only one statement inside the if block.
SendMessage() returns a string, so we can feed it directly to the print or say function.
Here is how I implemented the SendMessage() subroutine:
# ------------------------------------------------------------------------------
# Name : SendMessage
# Comment : Sends message to chosen macroblogging site
# Input : $_[0] = Input string with value
# "twitter" -> twitter.com instance
# "identica" -> identi.ca instance
# $_[1] = Message string to be sent
# $_[2] = User name (login)
# $_[3] = Password
# Output : Return string: "Error" if couldn't create object or string from SendUpdate
# ------------------------------------------------------------------------------
sub SendMessage {
my $ReturnString;
my $Instance;

$Instance = CreateObject($_[0], $_[2], $_[3]);
if ($Instance) {
$ReturnString = SendUpdate($Instance, $_[1]);
}
else {
$ReturnString = "Error with $_[0] creation process";
}
return $ReturnString;
} #End of SendMessage
New Perl concepts introduced by SendMessage():
  • A subroutine if defined by the sub keyword followed by the function name and a {} block
  • Arguments are passed to a function between parenthesis and are separated by commas: subroutine(arg1, arg2);
  • Arguments are retrieved by the function inside the @_ array. An array is a list of individual values. As scalars are prefixed by the $ symbols, arrays are recognized by the @ sign preceding their name.
  • Individual values can be accessed within a list. For example, the second element of @ToDoList is $ToDoList[1] (subscripts start from 0)
  • $_[0] is therefore the first parameter passed to the function. In the SendMessage example, that would be the website identifier ("twitter" or "identica").
  • You can only return one scalar or one list directly from a function by means of the return keyword.
  • In the SendMessage example, $ReturnString and $Instance only exist within the subroutine's {} block because of the my keyword. They cease to exist outside of SendMessage's scope.
In SendMessage, we call two other functions:
  • CreateObject() which will return the newly created Net::Twitter instance. It is inside CreateObject() that we will differentiate the twitter and identica cases.
  • SendUpdate() will send the message to the twitter or identica instance.
CreateObject() introduces a few new Perl contructs:
# ------------------------------------------------------------------------------
# Name : CreateObject
# Comment : Creates and returns an instance of Net::Twitter
# Input : - Input string with value
# "twitter" -> twitter.com instance
# "identica" -> identi.ca instance
# all other values return an error
# - User name (login)
# - Password
# Output : Object newly created or 0 if error
# ------------------------------------------------------------------------------
sub CreateObject {
my $SiteInstance = 0;
my $NameString = shift;
# After shift, UserName parameter becomes $_[0] (was $_[1] before)
# and Password parameter becomes $_[1] (was $_[2] before)

$NameString =~ tr/A-Z/a-z/;
if ($NameString eq "twitter") {
$SiteInstance = Net::Twitter->new(username => $_[0], password => $_[1]);
}
elsif ($NameString eq "identica") {
$SiteInstance = Net::Twitter->new(identica => 1, username => $_[0], password => $_[1]);
}
return $SiteInstance;
} # End of CreateObject
New Perl concepts introduced by CreateObject():
  • First one is the shift operator. It removes a single element from the argument list. So if we have @_ = ("site", "username", "password") passed as parameters to CreateObject, the line my $NameString = shift; will store "site" in $NameString and @_ will become ("username", "password").
  • Hence, username which used to be referred to as $_[1] now becomes $_[0]
  • $NameString =~ tr/A-Z/a-z/; uses the transliteration operator tr///. It transforms a set of characters (A-Z: means ASCII characters 'A', 'B', ..., 'Z') into another set of characters (a-z: means ASCII 'a', 'b', ..., 'z'). The transliteration is bound to a string via the =~operator. In our example, we make sure that $NameString only contains lowercase characters for easy comparison.
The SendUpdate sub is pretty straightforward:
# ------------------------------------------------------------------------------
# Name : SendUpdate
# Comment : Sends update to Twitter object
# Input : - Net::Twitter object
# - message string
# Output : string "OK" if successful update, string "FAIL" otherwise
# ------------------------------------------------------------------------------
sub SendUpdate {
my $Site = shift;
my $Message = shift;
my $SiteName = "twitter.com";

$SiteName = "identi.ca" if ($Site->{identica});
#There's a hard limit on the size of twits for both twitter and identica
if (length $Message > 140) {
return "$SiteName update: FAILED (message over 140 characters)";
}

if ($Site->update($Message)) {
return "$SiteName update: OK";
}
else {
return "$SiteName update: FAIL";
}
} #End of SendUpdate
New Perl concepts introduced by SendUpdate():
  • Nothing new really. Just note the weird construct of $Site->{identica}. For the moment, I won't analyze too much, suffice to know that when it is true, then we are dealing with an identi.ca object.
Well, that rounds it up for today: a lot of new things to review.
Version 0.0.2 doesn't add any functionality compared with v0.0.1 but has close to 50% extra code. Was it worth the effort? Probably not, as all identica-specific code is not located in one single function. However, each logical block is well separated now and can be reused through the program later.
Do you see any other way to improve twit.pl v0.0.2?

I have created a site with Google Sites to host all the scripts that I talk about in this blog. You can go take a look at http://sites.google.com/site/damienlearnsperl/DLP-scripts.
I also added a link to the side -->

Finally, I will intersperse (or even replace) French expressions of the day with Larry Wall quotes (taken from here and here for example) and other sayings by Perl personnalities.

Larry Wall quote of the day:
"The three chief virtues of a programmer are: Laziness, Impatience and Hubris."

Possible next posts:
  • Improving on twit.pl: reading from file
  • How to install and use Google Analytics on your Blogger blog
  • Improving on twit.pl: using more of the Net::Twitter API
  • Improving on twit.pl: Graphical User interface
  • Perl help resources
  • POD

1 comment :

  1. Instead of using this

    my $SiteName = "twitter.com";

    $SiteName = "identi.ca" if ($Site->{identica});


    you could also use a conditional assignment

    $SiteName = ($Site->{identica})?"identi.ca":"twitter.com";

    ReplyDelete