Friday, October 8, 2010

"Importing" Static Functions for use in Scala Subclasses

I'm writing some WebDriver web tests at the moment, using the PageObjects pattern. This is a pattern where you have a class in your test source tree representing each page on your site and each of these classes exposes all the data and functions of the page it represents, using the domain language of your application and hiding the HTML stuff from the test.

As well as having the tests clear of HTML knowledge, I like to keep the PageObject classes as simple as possible when I can. WebDriver is very handy and succinct, but I like to go even further, so instead of my PageObject containing:
driver.findElement(By.id("submit")).click
I would much prefer to just write:
click(id("submit"))
I already have an abstract base class called Page, so the solution is simple enough: write a click() function in the base class and somehow import all the methods in org.openqa.selenium.By.

Being super-lazy, I don't really want to have to add import org.openqa.selenium.By._ to the top of every Page subclass, especially seeing as I know I'll need them in EVERY subclass. I want the Page to provide this importing for me, so that the methods in By are automatically available in every subclass.

Failed Attempts
I tried a couple of solutions before I found what I believe is the laziest working approach. I tried just importing By._ in the Page class. This seemed too simple to work, and I was right. I also tried to import the methods with identical aliases (import By.{id => id}) but this didn't do anything either.

I thought maybe I could extend the By class and bring all the methods into the Page namespace that way, but the By class is both a factory for By instances as well as the abstract interface for those classes themselves, so I can't extend it without implementing the abstract findElements() method, which doesn't really make sense.

I concluded that explicit delegation was the only solution, so I resorted to writing this:
def id(id: String) = By.id(id)
That didn't seem too bad, but by the time I was half way through writing the second one, I decided that this was not lazy enough. I didn't want to have to retype the signature of all these functions.

A Suitably Lazy Solution
And then I remembered that I was working in a functional language, and that the functions I was delegating to were actually first-class values in Scala. So all I really needed was a way to provide an alias for those function values that allowed my subclasses to invoke the alias. The solution I came up with is to partially-apply each of the functions with no arguments and to make my alias functions return these partially-applied (though in reality completely unapplied) functions.

The resulting code looks like this:
def id = By.id _
def linkText = By.linkText _
def name = By.name _
def xpath = By.xpath _
Now my subclasses can use the functions id(), linkText(), name() and xpath() without qualification, and I didn't have to re-type the signatures of each one. It obviously didn't save me amazing wads of time in this case (especially now that I've written a blog entry about it) but in some other scenario where there are many methods with non-trivial signatures that need to be delegated, this could be a real time-saver.

What Does it Cost?
Note that there is a slight cost to what I've done here. Scala doesn't see what I've done and have its compiler automatically substitute the straightforward delegating method that I originally wrote. Instead, scalac creates a new class, a subclass of FunctionN, for each of these methods, with the classes having names like Page$$anonfun$id$1. The implementation of the Page.id() function that Scala outputs will create and return an object of this class, such that the caller then executes apply() on the Function, which in turn invokes the original method on By (statically at that point - no reflection). So my desire for laziness at the source level has resulted in some indirection at runtime, but this kind of simple functor creation is unlikely to cause any serious performance pain.

(If you were really worried about it, you could make the aliases vals instead of defs so that the functor objects would only be instantiated once, but then IDEA will start highlighting the usages of the aliases as fields instead of methods, which just looks weird.)

If you want to know more about the gory details of Partially-Applied functions, I would suggest grabbing a copy of "Programming in Scala" from O'Reilly from the Book Depository:
Programming Scala - Dean Wampler & Alex Payne (O'Reilly)
or from Amazon:

3 comments:

  1. You can also do
    def id = () => By.id

    which is basically identical to
    def id = By.id _

    seems to be a matter of style. The first style is "closuresesque" and the second is "partially applied" style.

    -- Eric

    ReplyDelete
  2. My bad - I didn't realize id takes parameters.

    It should have been def id = (x: String) = >By.id(x)

    The partially applied style is definitely cleaner.

    My understanding is that they both are interpreted as

    new Function1[String, By] { def apply(x: String) = By.id(x) }

    ps - I just made those style names up to be silly... thx for your post.

    ReplyDelete
  3. Thanks, Eric. I did try your suggestion and I couldn't get it to work, but I figured you must have just known something I didn't!

    ReplyDelete