Ruby in Atomo

Ruby provides three powerful mechanisms for modular code and simple definition of class structures: modules, classes, and mixins. Atomo doesn't have these built into the language at the design level, but all three are rather easily implemented using Atomo itself, building upon its simple object system.

This document will guide you through its implementation, which is entirely composed of macros; it should serve as a great introduction to metaprogramming in Atomo with macros.

Modules will be stored here, as Associations from the module's name, a string, to its contents, a list of expressions.

Here we use for-macro, a reserved word that tells the parser to evaluate this expression before the macroexpansion phase. We'll also use it later to define helper methods for our macros to use.

for-macro *modules* = []

Next, we'll need a nice syntax for defining modules. We can use a neat trick with Atomo's parser here - a: b: c is parsed as a: (b: c) - that is, one keyword dispatch a: with another, b: c, as its second target (the first being the implicit toplevel object).

We'll use module as the first keyword, and the keyword in the nested dispatch will be the name of the module. The second target in the nested dispatch will be the module's body, a block. Taking advantage of this, we get very simple module definition syntax, and also guarantee that a module can't be given some funky name.

Here's what it'll look like:

module: Foo: {
  -- body goes here
}

Registers the module, and just expands to the body block (though what it expands to isn't very important).

macro (module: (def: Dispatch))
  { name = def names head
    body = def targets at: 1
    add-module: name as: body contents
    body
  } call

add-module:as: is a small helper method for registering a module. We're using super here to specifically redefine the *modules* we created earlier.

for-macro add-module: (name: String) as: (body: List) :=
  super *modules* = *modules* set: name to: body

Now when we say module: A: { b := 2 }, a module A will be inserted into *modules*, associated with ['(b := 2)] - a list of its contents.

Nothing too useful yet - the fun begins when we include: it! So let's work on that.

Slurps a module, found via a given name, into its target, often the implicit toplevel object.

macro (target include: (name: Dispatch))
  include: name name on: target

Search for a module by name, panicking if it isn't registered. Returns an expression for defining the module's body onto the target.

for-macro include: (name: String) on: target :=
  *modules* (lookup: name) match: {
    @none -> error: @(unknown-module: name)

    @(ok: mod) ->
      { block =
          `Block new: (expand-body: mod on: `!top)
            arguments: [`!top]

        `(~target join: ~block with: ~target)
      } call
  }

It's important to note that we're using special names here, notably !top, to avoid name collision. These names are guaranteed to be safe; each time the macro expands, identifiers in a quasiquote beginning with an exclamation mark are decorated with a clock value, so there's no way for it to be confused with some other name from outside the expansion.

Maps over a given list of expressions, expanding its contents into their module or class forms.

for-macro expand-body: (exprs: List) on: target :=
  exprs map: { e | expand-expr: e on: target }

Come up with a method definition expression that has a the current instance as the method body's context. Ruby and Java use self or this for explicitly referring to this value, but both of these names are taken in Atomo, so we'll go with me.

To be clear, here are the transformations it makes:

x := y
(me: { target }) x := me join: { y } 
x: y := y + 1
(me: { target }) x: y := me join: { y + 1 } 
new := { a = 1 }
(me: { target }) new := me clone do: { a = 1 } 
new: y := { a = y }
(me: { target }) new: y := me clone do: { a = y } 
new.foo: x bar: y := { a = x; b = y }
(me: { target }) new.foo: x bar: y :=
  me clone do: {
    a = x
    b = y
  }
for-macro expand-expr: `(~name := ~body) on: target :=
  { with-me =
      `Dispatch new: name particle
        to: (name targets at: 0 put: `(me: { ~target }))
        &optionals: name optionals

    if: (initializer?: name particle)
      then: { `(~with-me := ~target clone do: ~body) }
      else: { `(~with-me := me join: { ~body }) }
  } call

Expands an include: expression into the module's body, executed onto the given target (the class object or another module).

for-macro expand-expr: `(include: ~(y: Dispatch)) on: target :=
  include: y name on: target

Leave everything else alone.

for-macro expand-expr: e on: _ := e

Determine if a given method name looks like an initializer. True if the name is new, or if the first keyword is new or starts with new..

for-macro initializer?: (name: Particle) :=
  name type match: {
    @single -> name == @new

    @keyword ->
      name names head == "new" ||
        name names head starts-with?: "new."
  }

We already have a pretty useful module system, so let's give it a whirl!

module: Math: {
  -- an unexposed value
  some-helper = 10

  -- exposed methods
  pi := 3.14
  x := some-helper
}
> include: Math
@ok
> pi
3.14
> x
10

Ok, that seems to work. But that's very basic. Let's try including one module into another.

module: MoreMath: {
  include: Math

  another-helper = pi

  pi*2 := another-helper * 2
}
> include: MoreMath
@ok
> pi
3.14
> x
10
> pi*2
6.28

And what happens when we include some unknown module?

> include: FooBar
ERROR: <error @(unknown-module: "FooBar")>

Hooray! Delicious failure. Seems to all be in working order. So let's move on to classes.

Creates an anonymous class object, extending a given parent object, defaulting to Object.

macro (class: (b: Block) &extends: Object)
  `(~(class-create: b contents) call: ~extends clone)

Create a block for creating a class, given the class body (a list of expressions). This block takes a single argument: the target of the definitions, which is normally a clone of the parent object.

The block also defines me, so that a class's body can refer to the class object.

Again we're using a special name, !o, to prevent name collision.

for-macro class-create: (body: List) :=
  `Block new: (`(me = !o) . (expand-body: body on: `!o) .. [`!o])
            arguments: [`!o]

So now we have fully-functional (anonymous) classes with mixins. We can play around with this a bit already:

Greeter =
  class: {
    new: x := { name = x }
    say-hi := (name .. ": Hi!") print
  }
> Greeter (new: "John Smith") say-hi
John Smith: Hi!
"John Smith: Hi!"

That seems to be working pretty nicely. Now let's dig into class reopening! We'll be adding a second form of class:, this one taking a keyword dispatch very similar to the trick we used for module:. But now we can decide whether to define a new class or "reopen" an existing one.

This macro expands to an if:then:else: dispatch which checks if the class is in-scope. If it is, it simply calls the class creation body with the current class object, redefining all methods in the body. If it isn't defined, it just defines it.

macro (class: (c: Dispatch))
  { single = Particle new: c names head
    name = `Dispatch new: single to: ['this]
    body = c targets at: 1

    `(if: (responds-to?: ~single)
        then: { ~(class-create: body contents) call: ~name }
        else: { ~name = class: ~body } in-context)
  } call

Note that this works perfectly fine with arbitrary objects that weren't defined using our system. Everything expands to regular ol' objects and method definition - there is very little magic going on.

To prove it, I'll go ahead and reopen Number, the object representing numeric values in Atomo, and add a little plus: method which will just be an alias for +.

class: Number: {
  plus: y := + y
}
> 1 plus: 2
3

Ta-da!

So far I've shown classes working and modules working, but not classes and modules working. So let's give that a shot.

module: MyEnumerable: {
  all?: p :=
    { done |
      each: { x | (done yield: False) unless: (p call: x) }
      True
    } call/cc
}

class: LinkedList: {
  include: MyEnumerable

  new := { empty? = True }

  new.head: h tail: t :=
    { head = h
      tail = t
      empty? = False
    }

  from: (l: List) :=
    l reduce-right: { h t | LinkedList new.head: h tail: t }
        with: LinkedList new

  each: f :=
    if: empty?
      then: { @ok }
      else: {
        f call: head
        tail each: f
      }

  show :=
    if: empty?
      then: { "()" }
      else: {
        "(" .. head show .. " : " .. tail show .. ")"
      }
}

Here I've implemented a (very) small subset of the Enumerable module found in Ruby, and a basic linked list class. This class provides the necessary each: method, and includes the Enumerable module. Let's see if this works:

> x = LinkedList from: [1, 2]
(1 : (2 : ()))
> x each: { x | @(value: x) print }
@(value: 1)
@(value: 2)
@ok
> x all?: @(is-a?: Integer)
True
> x all?: @odd?
False

Looks good to me! Now that we have everything working, let's see what exactly these macros expand to.

Digging In

We'll use the ever-so-useful expand method to see what our macros are doing. Let's try it on a few variations of class:.

> `(class: { @ok }) expand
'({ o:1102 | me = o:1102; @ok; o:1102 } call: Object clone)
> `(class: { @ok } extends: Number) expand
'(class: { @ok } extends: Number)
> `(class: { new := { a = 1 }; is-one? := a == 1 }) expand
'({ o:1104 | me = o:1104;
             (me: { o:1104 }) new := o:1104 clone do: { a = 1 };
             (me: { o:1104 }) is-one? := me join: { a == 1 };
             o:1104 } call: Object clone)
> `(class: A: { @ok }) expand
'(if: (responds-to?: @A)
    then: { { o:1114 | me = o:1114; @ok; o:1114 } call: A }
    else: { A =
              { o:1117 | me = o:1117;
                         (me: { o:1117 }) pretty := me join: { Pretty text: "A" };
                         @ok;
                         o:1117 } call: Object clone } in-context)

As you can see, anonymous classes expand into a call to a class-creation block; the block is called with the cloned parent as an argument, and any methods are defined on it. The block then returns the class object.

We can also see that our bang-identifiers are being sprinkled with a bit of magic that keeps them from colliding with other identifiers. In fact, the parser rejects these names, but they're well past the parsing stage.

So what about including modules? What happens then? Let's see:

> `(include: Math) expand
'(join: { top:422 | some-helper = 10;
                    (me: { top:422 }) pi := me join: { 3.14 };
                    (me: { top:422 }) x := me join: { some-helper } }
    with: this)
> `(include: MoreMath) expand
'(join: { top:422 | top:422 join: { top:422 | some-helper = 10;
                                              (me: { top:422 }) pi := me join: { 3.14 };
                                              (me: { top:422 }) x := me join: { some-helper } }
                              with: top:422;
                    another-helper = pi;
                    (me: { top:422 }) pi*2 := me join: { another-helper * 2 } }
    with: this)

We see that including modules defines the module's contents onto a given target (in this case the toplevel object, this). This is done by @join:ing its body onto the target, passing an explicit reference to it along as an argument. Including one module in another does the same as anything else, but what about including a module into a class definition?

> `(class: { include: MyEnumerable }) expand
'({ o:1130 | me = o:1130;
             o:1130 join: { top:422 | (me: { top:422 }) all?: p :=
                                        me join: { { done | each: { x | unless: (p call: x) do: { done yield: False } };
                                                            True } call/cc } }
                      with: o:1130;
             o:1130 } call: Object clone)

Earlier we actually made include: inside of a class expand into something slightly different from the normal macro. The only thing we changed was that its target is the class object being built, rather than the context of the building. It's a bit tricky to explain, but basically we're making it so that include: SomeModule inside of a class body acts like me include: SomeModule.

Wrapping Up

In just a few lines of code we've written a simple, elegant, and low-overhead implementation of Ruby's module, class, and include forms. What's more, code written using these forms doesn't force others to adopt these idioms - everything expands into simple Atomo code that will be natural to any users of your API.

We've used a few things you should remember: for-macro for defining helper methods and values for your macros, macro for code that creates code, and the use of !bangs to achieve hygienic macros without any mental overhead. We've also briefly shown expand, a very useful method to use when writing macros to see what's really happening to your code.

You may have noticed something missing, though. You can't do this:

class: A: {} &extends: B

The implementation isn't too hard, though; just remember that it's actually parsed as class: (A: {} &extends: B). This is left as an exercise. Good luck!