Runtime PHP Annotations. What a tease.

Posted: May 30th, 2008 | Author: | Filed under: Coding | 7 Comments »

Today at work I found myself on the tedious side of software development. I mainly develop in PHP, and PHP lacks many things that would otherwise make it an enjoyable language to use. It seems like the mantra of the PHP developers and community as a whole is “half-assed” (in all due respect, of course ;) ).

Let me explain. If you want to use a library, tool, or feature in a way that the original developer didn’t care about or think about, then odds are that the documentation will claim it will work, but it won’t. It will work for the one case the developer tested, but that is it. Of course there are also many projects and features out there that will prove me wrong, and kudos to those ones.

I am getting sidetracked. Today I wanted annotations in PHP. I wanted to provide meta data for the methods and classes I was writing.

It would be easy enough to write a code generator that parses the source, handles the annotations, and spits out the desired code. In fact, many things already do this, like documenters and style checkers. However, that is not what I want. I want it done at runtime and I want it to change the behavior of the code.

Sometimes I have to back up my complaints with some action, so today I implemented this feature. In an entirely proof-of-concept way, but a real solution for runtime PHP annotations nonetheless.

Since examples speak louder than words, here we go.

<?php
 
class Pi {
 
  /**
   * Use the monte carlo method of estimating PI
   * @return float
   */
  public static function estimate () {
    $rounds = 1000000;
    $hits = 0;
    $max = mt_getrandmax();
    for ($i=0; $i<$rounds; $i++) {
      $x = mt_rand() * 1.0 / $max;
      $y = mt_rand() * 1.0 / $max;
      $hits += (($x*$x + $y*$y) <= 1.0)? 1: 0;
    }
    return 4.0 * $hits / $rounds;
  }
 
}
 
// Consistent return value
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";

Does anyone else enjoy naming a php variable ‘hit’ or ‘hits’? Anyway, for this example I wanted a function that would take some time to compute. Admittedly contrived, but I didn’t ask you to read this post. Using the monte carlo method, this static class method estimates the value of PI and is called twice.

time php Pi.php 
3.141196
3.141196
 
real    0m2.186s

Obviously, if many calls are made to this method, the time adds up quickly. No one in their right mind would actually release code like this. I said right mind. Something I find myself doing often is caching the result before returning it. I’m not going to show that example since it is trivial to implement. Clearly all subsequent calls to the time-consuming method would return immediately.

I don’t like it. I do it, but I don’t like it. The method starts out very clean. All it does is estimate the value of PI (or search a graph, or prune a tree, whatever). When you add caching into it, suddenly the method is doing two things. And two completely unrelated things at that. This should bother you at least a little bit. Off the top of my head, I can avoid this approach by:

  • Relying on consumer to cache the results. I don’t think so.
  • Extending the class with a new “Caching class”. Yuck. Not to mention the example above uses a static method.
  • Moving the two routines into to separate methods: public getEstimate() and private calculateEstimate(). getEstimate() would do the caching and call calculateEstimate() internally. Ok, but pretty tedious and causes the code to grow quickly.
  • Use annotations to modify the runtime behavior of the method.

What I really want to do is annotate the method, indicating that the operation is idempotent and that the results can be cached. I don’t want to design a whole framework right now, I’ll leave that for another day. To see if it can or can’t be done, I just want to implement this single feature for now.

<?php
 
class Pi {
 
  /**
   * Use the monte carlo method of estimating PI.
   * @cache
   * @return float
   */
  public static function estimate () {
    $rounds = 1000000;
    $hits = 0;
    $max = mt_getrandmax();
    for ($i=0; $i<$rounds; $i++) {
      $x = mt_rand() * 1.0 / $max;
      $y = mt_rand() * 1.0 / $max;
      $hits += (($x*$x + $y*$y) <= 1.0)? 1: 0;
    }
    return 4.0 * $hits / $rounds;
  }
 
}
 
require_once 'Annotations.php';
Annotations::annotate('Pi');
 
// Consistent return value
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";
mt_srand(0xDEAD);
echo Pi::estimate(), "\n";

Ideally, I’d like to add just that one line to the documentation comment above the method. Of course I’d need some way to trigger the parsing, so I put that at the end for the sake of the example. The code of the method remains solely dedicated to estimating the value of PI. I’ve only hinted that at runtime, it’d be great if the results were only calculated once.

It took less than twenty lines of code to implement this annotation. The methods of the class are retrieved, the annotation is searched for, and if found, the code of the method is altered to cache the results. (Actually I created a new class method that does the same thing, and rewrote the original one to do the caching only, calling the new method for the calculated value.)

Then I ran it, and sure enough it runs in half the time.

time php Pi.3.php 
3.141196
3.141196
 
real    0m1.175s

By the way, there are many attempts at a solution out there, none of which do what I want from what I can tell in 30 seconds of reading their summaries. The PHP reflection API seems to be “look but don’t touch”. I want to grope. I want a [@deprecated] method to raise a warning when the method is called. Likewise for a [@testing] method. I want my [@cache]. I want annotations for parameter validation… maybe that is a little too far, but you get the idea. The solutions I have seen let you poke and prod, but they don’t let you add/change functionality to the running code. Implementing these types of annotations would be pretty straightforward to do with the method I used above.

Since I made it work, and claim it was easy, why am I complaining? The problem is that the features I had to use to get it working are marked as unstable, unmaintained, subject to change, and risky to rely on for critical production code. The runkit extension doesn’t even compile for newer versions of PHP and some features even segfault.

So close. Half-assed.


Making a useful JavaScript library SUPER-USEFUL!

Posted: March 26th, 2008 | Author: | Filed under: Coding | 1 Comment »

Everyone likes (or should like) succinct-yet-readable code. Here is a technique you can use for taking your javascript modules and making them even more fun to use.

The following is a useful javascript module for getting and setting cookies. (It uses jQuery.) The usage is as straightforward as it gets. It provides three useful functions:

  1. Cookies.get(name);
  2. Cookies.set(name, value, [options]);
  3. Cookies.remove(name);

In addition, it provides the function Cookies.all() for debugging purposes.

var Cookies = {
 
  options :
    { path   : '/',
      expiry : null, // Days
      secure : (document.location.protocol == 'https:')
    },
 
  set :
    function (name, value, options) {
      var opts = {};
      jQuery.extend(opts, Cookies.options, options || {});
      options = opts;
 
      if (options.expiry) {
        // convert number of days from now to string
        options.expiry = new Date(
            new Date().getTime() + (options.expiry * 86400000)
        ).toGMTString();
      }
 
      // Build the cookie string
      document.cookie = name + '=' + value +
          ((options.expiry)? '; expires=' + options.expiry: '')
          + '; path=' + options.path + 
          ((options.secure)? '; secure=true': '');
    },
 
  get :
    function (name) {
      return $.trim($.grep(document.cookie.split(';'),
        function (cookie) {
          return $.trim(cookie).substring(0, name.length) == name;
        })[0] || name + '=').substring(name.length + 1);
    },
 
  remove :
    function (name) {
      return Cookies.set(name, "", -1);
    },
 
  all :
    function () {
      return document.cookie.split(';').map($.trim);
    }
 
}; // end Cookies

So what’s the big deal? The first weakness of this code is that the default options for setting a cookie are modifiable by the world. I don’t want your code changing settings for my code, nor mine for yours. This wouldn’t be too hard to fix by placing them as a local variable inside the set function. But what we’d really like is some sort of read-only access to those default options.

The second issue is more interesting. If you notice, there are four functions. And each of the functions takes a unique set of parameters. If this were Java, we could (but ought not to) implement this all with polymorphism. Sounds quite dumb, but bear with me. If the object Cookies were also a function, we could create a dispatcher and remove the need for explicit calls to the individual methods. Instead of Cookies.get('something'), we could just call Cookie('something'). Likewise for all the methods. Of course, you’d still be able to call the function by its full name. Let’s see it.

// Create a dispatcher
var Cookies = function (name, value, options) {
  // Case 1: no parameters supplied, dump the cookies.
  if (!name) {
    return Cookies.all();
 
  // Case 2: null value, delete cookie
  } else if (value === null) {
    return Cookies.remove(name);
 
  // Case 3: name supplied only, so get value
  } else if (value == undefined) {
    return Cookies.get(name);
 
  // Case 4: set a cookie with a value
  } else {
    return Cookies.set(name, value, options || {});
  }
 
}; // end dispatcher
 
$.extend(Cookies, (function () {
 
  // Create the private variables
  var defaults = {
    path   : '/',
    expiry : null, // Days
    secure : (document.location.protocol == 'https:')
  };
 
  // Create the public methods
  return {
    defaults : function () {
      return defaults;
    },
 
    set : function (name, value, options) {
      var opts = {};
      jQuery.extend(opts, defaults, options || {});
      options = opts;
 
      if (options.expiry) {
        // convert number of days from now to string
        options.expiry = new Date(
            new Date().getTime() + (options.expiry * 86400000)
        ).toGMTString();
      }
 
      // Build the cookie string
      document.cookie = name + '=' + value +
          ((options.expiry)? '; expires=' + options.expiry: '')
          + '; path=' + options.path + 
          ((options.secure)? '; secure=true': '');
    },
 
    get : function (name) {
      return $.trim($.grep(document.cookie.split(';'), 
        function (cookie) {
          return $.trim(cookie).substring(0, name.length) == name;
        })[0] || name + '=').substring(name.length + 1);
    },
 
    remove : function (name) {
      return Cookies.set(name, "", { expiry: -1});
    },
 
    all : function () {
      return document.cookie.split(';').map($.trim);
    }
  };
 
})());

The first thing you’ll notice is the dispatcher function. The does the simple logic for mapping the parameter combinations to the intended function. Then we extend the Cookies objects (remember, functions are just objects) with a new object. If you look, you’ll see that it is extended with a function that is immediately applied and returns an object. The advantage to doing this is that we can create closures, and encompass some “private” variables for those methods. Look at the defaults for an example of this. I added another function for inspecting the defaults, but you cannot modify them.

Now this library can be used as:

  • Cookies.get(name) or just Cookies(name)
  • Cookies.set(name, value, [options]) or just Cookies(name, value, [options])
  • Cookies.remove(name) or just Cookies(name, null)
  • Cookies.all() or just Cookies()
  • Cookies.defaults()

Hopefully this makes sense. If it gives you any good ideas, let me know.

Oh, and if you really were looking for a cookie library for javascript, you can download and use this (documented and commented) one if you have jQuery installed.

Download Javascript Cookies Version 1.0
Javascript Cookies (2.57 kB)