Skip to content

Instantly share code, notes, and snippets.

@baroquebobcat
Created February 8, 2015 03:09
Show Gist options
  • Save baroquebobcat/35b6f82cab6ad17bd82a to your computer and use it in GitHub Desktop.
Save baroquebobcat/35b6f82cab6ad17bd82a to your computer and use it in GitHub Desktop.
Mirah Extension Proposal

Extensions

Extensions are the way that Mirah adds methods to existing classes. When you call methods like each on collections, you are actually using extensions. They are helpful when you want to write DSLs and when you want to make a library you are using feel more Mirah-ish.

How do you use them?

Mirah comes with a number of built in extensions for Java's base types. For example, the methods each, map and empty are added to Collections. The built in extensions need no special invocation to be taken advantage of, they're already available. If you want to use extensions from a library, or optional extensions, you'll need to tell Mirah which ones you want to use.

The way you do that is by "using" the extension. using works just like import, but instead of making an imported class available, they extend other classes. For example, say you wanted to pluralize a string using an activesupport-like library. You could do it like so:

using org.dubious.active_support.StringInflections

fruit_count=5
puts "banana".pluralize(fruit_count)

The methods added by extensions are only visible by scopes that import them. If you pass an object that has extensions on it to a method defined in a different file, that extension won't be carried along with the object. It only exists in that scope. For example, the following code would be a compile error because pluralize only is available in uses_extension and not in no_extension.

def uses_extension
  using org.mirah_on_rails.active_support.StringInflections
  no_extension("banana")
end

def no_extension(fruit: String)
  puts fruit.pluralize(count)
end

Extensions are implemented as static methods on static classes, so they'll show up in your stacktraces. That also means that if you use extensions, you need to make sure that the associated runtime jars are on the CLASSPATH, or they will not work.

How are extensions different from interfaces? Extensions exist partially at runtime and partly at compile time. They don't affect the class that they extend, and they only available in scopes that are using them. Interfaces, on the other hand, are included as part of the class they are added to. If they have default implementations, the class defers to those methods, and any instance of that class will have that interface as part of its definition.

Writing Your Own Extensions

Extensions wouldn't be very interesting if they could only exist in Mirah's compiler code. They provide a way to change dispatch of methods.

extension HelloExt
  extending String do
    def hello
      puts "Hello, #{self}"
    end
  end
end

"Steve".hello

###Providing a DSL with extensions

Creating internal DSLs is easyish with extensions and macros. You can even make it so that the extensions are only in scope within a block kind of like a powerful instance_eval+using sort of thing. That might look something like this:

class App
  macro def config(block: Block)
    quote do
      using MyDSLExtensions
      result = `block.body`
      `@call.target`.config = result
    end
  end
end

extension MyDSLExtensions
  extending Numeric do # Numeric will work on primitive numbers somehow
    def mb
      "{self} in megabytes"
    end
  end
end

TODO adding a using like this should be hygenic--ie the quote should introduce a new scope so that using isn't added to the surrounding scope

What we're doing here is creating a config macro that takes a block, and introduces a using call that works on the body of the block. Users of this can import App, and use config with the config DSL without using MyDSLExtensions for their whole file. It could look something like this

import org.mycool.App

app = App.new
app.config do
  12.mb
end

Besides methods, extensions can also define macros. For example:

extension Foo
  extending Bar
    macro def baz
      quote { puts `@call.target` }
    end
  end
end

Helpers

Details

extensions are implemented as a combination of static classes with static methods and macros. As such producing an extension library is a little tricky. You'll want to produce two jars--classes and macros--if you want the resulting binaries to not require mirahc on their classpath for analysis. java and the jvm don't care because macro's dependencies on mirahc come through compile time annotations which are ignored by java. javac on the other hand, will get unhappy.

Extensions can't implement interfaces since they only exist as additional method lookup where they are used. Extensions can't have instance variables. Extensions can have inner classes, constants and class variables. They can't inherit from classes. Some of these could theoretically be loosened in the future.

Methods defined in extensions are static methods and can be called from the methods in extending blocks

extension Foo
  def bar
    1
  end
  extending Baz do
    def wut
      bar
    end
  end
end

Baz.new.wut == Foo.bar

Quirkily, you can import extensions as classes and use them as utility classes full of static methods directly.

import Foo

puts Foo.bar
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment