Tag the objects that you patch so you don’t break the host language

When creating an Internal Domain-Specific Language inside a language like Ruby you generally need to modify objects from the host’s core library -arrays or strings for example. Adding new methods to those objects is pretty straightforward but when you have to change the behaviour of an existing method other parts of the language can break since they weren’t expecting the new behaviour.

To be able to patch the host language’s core objects when creating your internal language but avoid breaking the host language’s ecosystem you should make your modifications be applied only in your language. To identify the a language construct is from your language or from the host language you should mark your objects with a tag and look for it.

The name of this pattern is a reference to Monkey Patching.

How it works

How to actually mark an object (or any other construct you are using) depends on the host language used. I’ve used this pattern in Ruby and Java (although you can’t extend most o Java’s core classes you still have some extensibility).

In Ruby generally you will make objects from your language defining a method that identifies them. The method can return some value (true/false or a symbol for example) or just be checked with respond_to? to see if the object has that method.

In Java there are two ways to do this. The first is using old-school tagging interfaces. Your object should implement an interface from your DSL and in the extension method you make an instanceof check. The more modern way –available in Java 5.0-and-later deployments- is using an annotation in classes and methods used by your DSL.

When to use it

When you modify a core method you should always be careful. Code from external libraries and from other –and often obscure- parts of the host language’s core probably won’t be expecting the modification and this could drive to unpredictable consequences. In dynamic languages this is even worse since you have no real control of what calls the code you modified. Languages that provide macros are moe flexible in their syntaxes, if you are using those probably you don’t need this pattern at all.

Whenever changing the core classes to build an Internal Domain-Specific Language it is mandatory to limit the scope of change. Monkey Tagging is a nice way of doing this.

Example: Defining a Log Literal

Registro’s Domain-Specific Language defines a log file line using several core constructs, like below.

['20080124', '01:20:00,018']  - (INFO "Something happened ")

In order to make this work we have to redefine the method Array#-, a Ruby core method. We really don’t care about other uses of that, we only want to change its behaviour when called from a Registro DSL call. In this situation the Array#- method receives the return of a DSL function, in the example would be the return of the INFO method. We will check this argument for a tag.

So we tag the returned object:

def self.method_missing(name, args)
	# INFO looks like a lo level (just like WARN or DEBUG)
            if(looks_like_log_level? name)
	    #the object that will be returned
                r = { :level => name.to_s.downcase.to_sym, :text => args}
	    #tag it by adding a method
                def r._registro_dsl?
                    true
                end
            else
                r = super.original_method_missing name, args
            end
            r
        end

And check for the tag in the Array extension:

class Array
    alias  :original_minus :-

   # A separator for parts in a log entry. Will return an array containing self + the argument
    def -(arg)
	#if argument has the tagging method do the magic, otherwise do the old logic
        (arg._registro_dsl?)? [self, arg] : original_minus(arg)
    end
end

Notice that this code would result in a NoMethodError for objects that are not part of our DSL. We could change this to use respond_to? but instead we prefer to define this method into all object, thus we will define a default implementation on the Object class.

def Object._registro_dsl?
	False
end

So not every object created will be from the DSL domain by default.

History
31/01/2008 - First Draft