6/06/2013

Groovy Mixins and the undocumented features of this pointer

I've been using Groovy and Grails lately and I love the platform. It's a great productivity tool. However, the docs for Groovy the language are languishing and haven't been kept up to date as the platform has evolved. One of those evolutions that is poorly documented is Mixins and I'm specifically talking about dynamic Mixins. Compile time Mixins use the annotation and there are several versions using @Mixin and @Category, but essentially the are limited in their use because you can't add a mixin into a class you didn't author. That means you have to use a different mechanism to augment 3rd party classes. This leaves either modifying the metaClass property on the class or using the newer Dynamic Mixin feature.

For example let's say we want to add a zip method on java.util.File. This method would take this File instance and produce a zipped version of it. For files it simply compresses the file, and for directories it's compress the whole directory and return the resulting file. Using the metaClass property we could do the following to add this:

File.metaClass.zip = { String destination ->
   OutputStream result = new ZipOutputStream(new FileOutputStream(destination))
   result.withStream { ZipOutputStream zipOutStream ->
   delegate.eachFileRecurse { f ->
       if (!f.isDirectory()) {
            zipOutStream.putNextEntry(new ZipEntry(f.getPath()))
            new FileInputStream(f).withStream { stream ->
                zipOutStream << stream
                zipOutStream.closeEntry()
            }
       }
   }
}

This works well and now you can do something as simple as new File( 'some/directory').zip('some_directory.zip'), and boom it writes out a zipped copy of that directory! That's pretty awesome isn't it? I think you're seeing the reason for why we want to do this.

Now let's see if we can translate that into a dynamic Mixin. Here is the version in Mixin form:

class EnhancedFile {

    static {
        File.metaClass.mixin( EnhancedFile )
    }

    void zip( String destination ) {
        OutputStream result = new ZipOutputStream(new FileOutputStream(destination))
        result.withStream { ZipOutputStream zipOutStream ->
            eachFileRecurse { f ->
                if (!f.isDirectory()) {
                    zipOutStream.putNextEntry(new ZipEntry(f.getPath()))
                    new FileInputStream(f).withStream { stream ->
                        zipOutStream << stream
                        zipOutStream.closeEntry()
                    }
                }
            }
        }
    }
}

Some small changes were made to the code. One is the static block at the top now places the mixin into the File object when this class is loaded. This is where Mixins added to 3rd party could be better. Essentially I just want to add this to augment 3rd party libraries, and it could be added at compile time through a simple annotation that let's me annotate the Mixin instead of the target of the Mixin. For example if I could use @MixinTarget(File) on the Mixin to augment File it could register it at compile time, but sadly it doesn't exist. This is why were are using runtime mixins here.

The other change was removing the delegate member. In metaClass mixin land delegate is a magic keyword that points back to the target of the mixin, or the instance your code was mixed into. In Dynamic Mixin land delegate keyword doesn't exist. However, you can refer to methods in the target class by calling them as if they were instance methods on the Dynamic Mixin. Notice how File.eachFileRecurse() method is called within the mixin.

This is our first clue how Dynamic Mixins are different than metaClass mixins. In dynamic mixin land delegate is not defined so referring back to the target is undocumented! There is no discussion about how it works or how its suppose to work. This is the point of this blog post.

Now let's say we want to add an unzip method to our Mixin. Let's look at the metaClass version first:

File.metaClass.unzip = { File destination ->
   ZipFile zf = new ZipFile( (File)delegate )
   Enumeration entries = zf.entries()
   while( entries.hasMoreElements() ) {
       ZipEntry entry = entries.nextElement()
       File f = new File( destination, entry.name )
       if( !f.getParentFile().exists() ) f.mkdirs()
           new FileOutputStream( f ).withStream { OutputStream stream ->
               stream << zf.getInputStream( entry )
           }
       }
   }
}

File.metaClass.unzip = { String destination ->
    return delegate.unzip( new File( destination ) )
}

In this example I have two overloaded versions of the unzip method. That's cool because Groovy honors Java's call differentiation by type, but the crux of this method is in the first one. It's pretty straight forward unzips this File instance into the destination File instance. See any issue with porting? That first line is passing the target of the mixin using delegate keyword to ZipFile! How can we implement that in a Dynamix Mixin!? This is the confusing part. In Dynamic Mixin land what does this pointer point to? Why it points to the instance of the Mixin. In this case its an instance of EnhancedFile. Well that doesn't do us much good does it? But what is the relationships between Mixin and Mixee? That gets a bit fuzzy. We could try casting this to a File after all it appears this is a File because we can simply call instance methods as if they were inside EnhancedFile too. Let's try that:

    ZipFile zf = new ZipFile( (File)this )

But that doesn't work and throws a ClassCastException. What about using the as keyword to convert it?

    ZipFile zf = new ZipFile( this as File )

That actually works! And here is a simple test you can try out:

    class MeMixin {
        def me() {
           return this
        }
    }

    class MeTarget {
    }

    MeTarget.mixin MeMixin

    target = new MeTarget()
    println( target.equals( target.me() as MeTarget ) )
    println( target.equals( target.me() )

The above code will print true then false. So the as keyword somehow changes the this pointer of the Mixin into the target class. It's the same reference as the original (that's important). Well it'd be pretty useless if it wasn't. Now why this works I can't explain that yet.

Here is the full code:

class EnhancedFile {

    static {
        File.metaClass.mixin( EnhancedFile )
    }

    void zip( String destination ) {
        OutputStream result = new ZipOutputStream(new FileOutputStream(destination))
        result.withStream { ZipOutputStream zipOutStream ->
            eachFileRecurse { f ->
                if (!f.isDirectory()) {
                    zipOutStream.putNextEntry(new ZipEntry(f.getPath()))
                    new FileInputStream(f).withStream { stream ->
                        zipOutStream << stream
                        zipOutStream.closeEntry()
                    }
                }
            }
        }
    }

    void unzip( File destination ) {
        ZipFile zf = new ZipFile( this as File )
        Enumeration entries = zf.entries()
        while( entries.hasMoreElements() ) {
            ZipEntry entry = entries.nextElement()
            File f = new File( destination, entry.name )
            if( !f.getParentFile().exists() ) f.mkdirs()
            new FileOutputStream( f ).withStream { OutputStream stream ->
                stream << zf.getInputStream( entry )
            }
        }
    }

    void unzip( String destination ) {
        unzip( new File( destination ) )
    }
}