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 Here we 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 - We'll use 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). | |
| |
Now when we say Nothing too useful yet - the fun begins when we | |
Slurps a module, found via a given | |
Search for a module by name, panicking if it isn't registered. Returns an expression for defining the module's body onto the target. | |
It's important to note that we're using special names here, notably | |
Maps over a given list of expressions, expanding its contents into their module or class forms. | |
Come up with a method definition expression that has a the current instance as the method body's context. Ruby and Java use To be clear, here are the transformations it makes: | 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 | |
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 | |
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. > 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 | |
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 Again we're using a special name, | |
So now we have fully-functional (anonymous) classes with mixins. We can play around with this a bit already: > 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 | |
This macro expands to an | |
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 > 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 > 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 > `(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, > `(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 | |
Wrapping Up | |
In just a few lines of code we've written a simple, elegant, and low-overhead implementation of Ruby's We've used a few things you should remember: 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 |