One of the primary design goals of Latitude is to provide a strong metaprogramming toolset. In this chapter, we’ll be discussing several of the tools for reflection and metaprogramming that Latitude gives programmers access to.
The first tool we’ll discuss is minor but very handy. The Latitude
root object provides a tap
method which takes a block. When an
object receives the tap
message, it runs the block with itself as
the receiver, then returns itself. It is primarily used to initialize
new objects. This is best demonstrated with an example.
myObject := Object clone {
self toString := "myObject".
self pretty := "My shiny object :)".
self someMethod := {
;; ...
}.
}.
In this way, we can construct a new object and initialize all of its behaviors in one statement, organizing all of the functionality into one nice, indented block.
Latitude provides several methods on the root object to modify slots
based on runtime information. slot
can be used to access a slot
based on a runtime value, slot=
can be used to assign, and slot?
can be used to check for slot existence. If you recall, variable
scopes in Latitude are objects like any other, so we can use these
methods to dynamically create variables.
define := {
lexical caller slot ($1) = Nil.
}.
define 'var.
println: var. ; ==> Nil
Here, we’ve created a method define
which, when called, defines a
new variable in the caller’s scope. On its own, define
is not
terribly useful, but we can expand it to include an accessor.1
define= := {
;; Define the slot variableName and the accessor variableName=
target := self.
var := $1.
target slot (var) = $2.
accessor := (var asText ++ "=") intern.
target slot (accessor) = { target slot (var) = $1. }.
}.
define := {
self define ($1) = Nil.
}.
First, rather than using lexical caller
, we simply refer to self
,
since define
(and, in this case, define=
) is a message being
passed to the lexical scope anyway. Then, in addition to making the
variable, we make an accessor whose name ends in an =
. So
define 'var = 0.
will define a variable named var
in the current lexical scope and
also a method var=
which assigns to that variable. This particular
syntax may look familiar to you; that’s because it’s exactly the
local
syntax for declaring local variables. In fact, the code shown
above is very close to the way local
and local=
are actually
implemented in Latitude.
Likewise, we can define the built-in localize
method using the same
techniques. Recall that localize
binds the value this
to be the
current value of self
.
myLocalize := {
self this := #'(self self).
}.
Inside of myLocalize
, self
is the lexical scope which received the
message, so self self
is the caller’s self
value.
We have already seen that you can use $*
to get the current argument
list as an iterable collection. However, argument lists can be
manipulated more directly and even mutated. Argument lists can be
constructed with ArgList clone
and filled with fillWith
.
;; The following are equivalent.
args := $*.
args := ArgList clone fillWith ($dynamic).
Once you have the argument list object (either by fillWith
or $*
),
you can manipulate it by shifting values on or off it. Mutating an
argument list only affects the scope it was created in, so you don’t
have to worry about messing up your caller’s dynamic scope. shift
will rotate the first argument onto the end of the argument list and
return the rotated value. unshift
will rotate the final argument
onto the front of the argument list and return the rotated value.
printArgs := {
$* visit {
println: $1.
}.
}.
shiftFirstArg := {
$* shift.
printArgs. ; Forward the rotated argument list
}.
shiftFirstArg: 1, 2, 3, 4, 5. ; ==> Prints 2, 3, 4, 5, 1
Similarly, you can push values onto front of the argument list with
unshiftOnto
, which takes an argument and places it at the front of
the argument list.
The inverse operation of fillWith
is dropInto
, which takes an
argument list object and places its arguments into the given scope.
printStoredArgs := {
;; Takes an argument list and drops it into the current scope
$1 dropInto ($dynamic).
$* visit {
println: $1.
}.
}.
storeArgs := {
$*.
}.
argList := storeArgs (1, 2, 3, 4, 5).
argList map! { $1 * 10. }. ; Let's modify the argument list object in-place
printStoredArgs (argList). ; ==> Prints 10, 20, 30, 40, 50
Now we have the machinery to implement the takes
syntax. Recall that
takes
binds dynamic arguments to given lexical names.
myTakes := {
;; Get the caller's dynamic scope
args := ArgList clone fillWith ($dynamic parent).
;; We are defining names in the caller's lexical scope
target := lexical caller.
;; Pair each of the names given with an argument
$1 clone zip! (args) map! {
;; For each pair (given by zip! as a cons cell),
;; assign the name
target slot ($1 car) = $1 cdr.
}.
}.
;; Sample usage
myMethod := {
myTakes '[a, b, c].
a + b + c.
}.
println: myMethod (1, 2, 3). ; ==> Prints 6
Often, we would like to override a method while also preserving (but
slightly augmenting) the parent method’s behavior, similar to calling
super()
in some other languages. Latitude provides this
functionality via Parents above
.
myObject := Object clone tap {
self clone := {
Parents above (myObject, 'clone) call tap {
;; Customize the new object
self someValue := 0.
}.
}.
}.
Parents above (myObject, 'clone)
returns a procedure object which
will invoke the clone
method on myObject
’s parent2. We
call that procedure, then “tap” the resulting object to customize it.
Note that we explicitly pass myObject
to Parents above
rather than
self
. We do not want the method above self
(who could feasibly be
an indirect subobject of some other subobject); we want the particular
implementation of clone
which falls above myObject
. If we had
passed self
and someone else along the inheritance hierarchy had
also passed self
, the Parents above
calls would get stuck in a
loop calling the method above self
over and over again. Thus, to
make this behavior sustainable in a larger inheritance hierarchy, we
pass the object we want to target explicitly.
What happens if we try to access a slot which does not exist on an object? Surely, you’ve already run into this, either accidentally or intentionally.
% Object clone thisSlotDoesNotExist.
***** EXCEPTION *****
SlotError - Could not find slot 'thisSlotDoesNotExist
<stack trace omitted>
Appropriately, we get an error. However, it is possible to override
this behavior. If an object does not know how to respond to a message,
the Latitude VM will fall back to sending the missing
message, with
the symbol that was originally intended as an argument. The value
returned from the missing
message will be used as the slot’s value
for this instance only. So we can effectively override the “lookup
failed” behavior.
mySafeObject := Object clone tap {
self missing := Nil.
}.
println: mySafeObject thisSlotDoesNotExist. ; Prints Nil
A missing
method which does not wish to emulate the slot should call
the parent’s missing
method explicitly, rather than raising an
exception.
mySafeObject := Object clone tap {
self missing := {
takes '[sym].
if (sym == 'foo) then {
Nil.
} else {
Parents above (mySafeObject, 'missing) call (sym).
}.
}.
}.
For a useful, real-world example of missing
, we need to look no
further than Latitude’s module system. When a module’s names are
imported with importAll
, a new object is injected into the local
scope, and that new object has a missing
method which forwards to
the module.
myImportAll := {
takes '[module].
;; Make a new object in the lexical scope hierarchy
scope := lexical caller.
scope parent := scope parent clone.
target := scope parent.
target missing := {
takes '[sym].
{
module slot (sym).
} catch (err SlotError) do {
Parents above (target, 'missing) call (sym).
}.
}.
}.
To import all of the names into scope, we define a new object in the
inheritance hierarchy. That new object has a missing
method which
explicitly delegates to the module.
+----------------------+
| Old lexical parent | +----------------------+ +----------------------+
+----------------------+<--| New object |------------->| Target module |
^ +----------------------+ Delegates to +----------------------+
X ^
X /
+----------------------+ /
| Lexical scope |/
+----------------------+
We’ve already briefly mentioned sigils, when using the ~fmt
sigil to
create a format string. Now we’ll discuss how to create custom sigils.
The meta
slot on a lexical scope object stores the current meta
object. The current meta object is used for several purposes, not
least of which is sigil lookup.3 When an expression of
the form ~name (someExpr)
is encountered, it is converted to the
call
meta sigil name (someExpr).
As with ordinary method calls, if the argument is a literal, the
parentheses may be omitted, so ~name 1
desugars to meta sigil name
(1)
. Sigils desugar to ordinary method calls, so they are merely a
syntactic convenience. Aside from ~fmt
(in the 'format
module),
Latitude defines a few other built-in sigils. One of the most useful
is the ~l
sigil for lazy evaluation.
value := ~l {
putln: "Doing some expensive calculations... :)".
42.
}.
putln: "Haven't done any work yet.".
println: value. ; Doing some expensive calculations... :) 42
println: value. ; 42
~l
takes a block and returns a new method. The new method will call
the block and cache the result the first time it is invoked. When it
is later invoked, it will return the cached value. In effect, it
constructs a method which emulates lazy evaluation, and since Latitude
methods are invoked the same way variables are accessed, you can treat
it the same way you would treat a variable.
Sigils are stored in the meta sigil
object. When importing a module,
the meta
object is never imported, even with importAll
. For this
reason, if you wish to provide sigils as part of a module, you must
store them in a different place. Every module object has a sigils
slot, to which you can attach arbitrary sigils.
;;* MODULE sigil-module
;;* PACKAGE com.example.latitude.tutorial
sigil-module := $whereAmI.
meta sigil mysigil := { ... }.
sigil-module sigils mysigil := #'(meta sigil mysigil).
sigil-module.
Then, when importing the module, you can use importAllSigils
to
explicitly import sigils into the current meta.
use 'sigil-module importAllSigils.
Normally, when we send a message to an object, we send a symbolic
name, and the object chooses how to react to the message. Thus, the
same name may behave differently depending on which object is
receiving the message. A prime example of this is toString
. Clearly,
sending toString
to a numerical object is going to have different
behavior than sending toString
to a string object. However,
sometimes it can be useful to send an explicit set of instructions to
an object, rather than a name. This can be done with send
. send
returns a procedure object which, when called, will invoke its block
with the appropriate caller bound as self
.
foo ::= Object clone.
foo send {
println: self. ; Prints foo
} call.
send
returns a procedure object so that you can explicitly pass
arguments to the constructed procedure if you wish.
use 'format importAllSigils.
foo ::= Object clone.
foo send {
$stdout printf: ~fmt "Self is ~S and args are (~S, ~S)", self, $1, $2. ; foo, 10, 20
} call: 10, 20.
More commonly, you will not pass an explicit block to send
but
rather a method passed in from a caller. For instance, tap
is
implemented in terms of send
.
myTap := {
takes '[obj, mthd].
obj send #'(mthd) call.
obj.
}.
It can be handy to set up a variable scope before performing a call.
For instance, recall that loop*
behaves like loop
except that it
also defines next
and last
in the loop body’s scope. This behavior
is also implemented in terms of send
. By calling by
on the
procedure object, we can supply a setup method. This setup method will
be called before the actual method is called, and it will take two
arguments: the future lexical scope and the future dynamic scope. We
can freely modify these arguments, and those changes will be reflected
in the method call.
For brevity, we’ll only be implementing last
in our loop, but next
would be similar.
myLoop* := {
takes '[block].
callCC {
escapable.
loop {
Conditional send #'(content) by {
$1 last := { return. }.
} call.
}.
}.
}.
We use the Conditional
object as the target of the method. There is
not any particular reason for this; we just need some object to be the
receiver, and a singleton flow-control-centric object would seem to be
a sensible candidate. Before running content
, the call invokes our
setup method, which defines a lexically-scoped last
method. Remember
that the setup method is passed two arguments: lexical scope and
dynamic scope. So $1
is the lexical scope argument, on which we
define last
.
Latitude’s strength comes in its ability to define new control structures and influence the language in subtle ways. Now you’ve seen many of the tools used to do so.
[up]
[prev - Mixin Objects]
[next - Closing Thoughts]
1 asText
converts a
symbol into a string representing the symbol’s name. intern
takes a
string and returns an appropriate symbol. These methods are not quite
inverses of one another, for reasons we haven’t discussed yet. arg
intern asText
will always return the same string originally passed
in, but arg asText intern
may return a distinct symbol in certain
cases.
2 Specifically,
above
finds the slot with the given name on the object, ignores that
value, and accesses the slot on that object’s parent, essentially
ignoring the first layer of lookup.
3 The meta object is also used for operator precedence resolution. Customizing the operator table is not explicitly discussed in this tutorial, but you can read about it in the Latitude specification at Chapter 2 - Lexical Structure.