At this point, we’ve talked about how to construct new objects and define slots on them. Now we’ll discuss how local variables and scopes work in Latitude.
We haven’t explicitly talked about local variables yet. We already
know how to use them; we just haven’t explicitly said so yet. This is
because local variables are no different than slots, so we can define
them using the same :=
syntax.
% theNumberOne := 1.
1
% theNumberOne.
1
It’s worth noting that Latitude is fairly liberal in what it allows as a variable name. The following characters are never allowed in variable names.
.,:;()[]{}'"`\
And the following characters cannot begin a variable name but can appear in the middle of one.
~#@0123456789
Aside from these two rules, almost any nonempty string of printable, non-whitespace Unicode characters is a valid identifier. There are a few small exceptions, which are detailed in the language specification at Identifiers, but it is not necessary to know them all. All of the following are valid variable names.
myShinyNewObject
isInteger?
**
foo/bar
<<m=~m>>
So, although it is not recommended from a readability standpoint, the following is a perfectly valid Latitude script.
<<m=~m>> := "Hello, world!".
putln (<<m=~m>>).
Now, while we could define all of our variables using :=
and be
perfectly happy, it does create some difficulties when it comes to
inner scopes. Consider the following script.1
i := 1.
addOne := {
i := i + 1.
}.
println (i).
addOne.
println (i).
addOne
is a method which, we would hope, adds one to the variable
i
. Intuitively, one might think that this would print 1
and then
2
. However, actually running this script produces the initially
counterintuitive result
$ latitude add-one.lats
1
1
As it turns out, :=
always assigns a variable in the current
scope. So when we do i := i + 1
inside addOne
, we are making a
new local variable inside that scope whose value is i + 1
. We’re
not modifying the original local variable.
We can get around this by declaring the variable to be a local
variable, which also defines an accessor. Notice the local
declaration on the first line, and the change from :=
to =
on the
third.
local 'i = 1.
addOne := {
i = i + 1.
}.
println (i).
addOne.
println (i).
Now the script behaves the way we’d like it to.
$ latitude add-one.lats
1
2
local
defines a variable, just like :=
, but it also defines an
accessor which can be used to mutate that variable via =
. We’ll have
the tools to talk about this more specifically soon, but for now just
remember to annotate all your local variables with local
.
We need to make a brief aside here about lexical and dynamic scoping. The difference is best exemplified with a snippet of pseudo-code.
x <- "Lexical"
def foo()
print x
end
def bar()
x <- "Dynamic"
foo()
end
bar()
The question of lexical vs dynamic scoping is what determines whether this pseudo-program outputs, appropriately, “Lexical” or “Dynamic”.
In a lexically scoped language, the x
in foo()
would bind to the
global declaration of x
whose value is "Lexical"
, which is the
narrowest enclosing scope containing the variable. In a
dynamically scoped language, the x
in foo()
would bind to
bar()
’s declaration of x
whose value is "Dynamic"
, which is the
caller’s scope.
That is, a lexically scoped language always looks for variables in the physically enclosing scope, according to the placement of the source code, while a dynamically scoped language traces up the call stack looking for variables.
These days, most modern languages subscribe to lexical scoping, as it is more readable, more intuitive, and easier to compile. However, dynamic scoping does have its merits in specific situations. For this reason, Latitude primarily uses lexical scoping but also supports dynamic scoping.
By default, variables in Latitude are lexically scoped. Let’s translate that pseudo-code into Latitude and verify this.
local 'x = "Lexical".
foo := {
putln (x).
}.
bar := {
local 'x = "Dynamic".
foo.
}.
bar.
As expected, running this script will print “Lexical” to the screen.
To enable dynamic scoping, we prefix the variable name with a $
. Any
variable whose name begins with a $
is dynamically scoped in
Latitude. Additionally, since dynamic variables are seldom mutated
in-place, the local
method will never make a dynamic variable, so we
need to switch back to the :=
syntax.
$x := "Lexical".
foo := {
putln ($x).
}.
bar := {
$x := "Dynamic".
foo.
}.
bar.
Running this modified script will correctly print “Dynamic” to the screen.
Consider the defining behavior of scopes.
First, consider a lexically scoped language. When a variable is accessed, the current scope is checked. If the variable does not exist, the enclosing lexical scope is recursively checked. This continues until either the variable is located or the global scope is reached.
Likewise, consider a dynamically scoped language. When a variable is accessed, the current scope is again checked. If the variable does not exist, the caller’s dynamic scope is recursively checked. This continues until either the variable is located or the global scope is reached.
Both of these processes seem superficially similar to Latitude’s
inheritance lookup algorithm. In Latitude, this similarity is
codified, as scopes are Latitude objects. You can access the current
lexical and dynamic scope objects by, respectively, lexical
and
$dynamic
.
% lexical.
#<Scope>
% lexical foo := 1.
1
% foo.
1
% $dynamic $bar := 2.
2
% $bar.
2.
Being able to treat variable scopes as objects opens up an entirely
new dimension of metaprogramming possibilities. The parent
of a
lexical scope is the enclosing lexical scope, and the parent
of a
dynamic scope is the caller’s dynamic scope. Additionally, the
caller
method on a lexical scope object will return the caller’s
lexical scope, allowing methods to define variables lexically within
their caller.
To belabor the point, there is no distinction between a slot on the scope object and a “local” variable. The two concepts are one and the same in Latitude, and to access a local variable, we send a message with the variable’s name to the scope object. This is also why we can call variables containing a method by just using the name.
foo := { putln "It works!". }.
;; We are sending the message foo to the lexical scope object, which calls the
;; method stored at that slot.
foo.
;; Output: It works!
As before, if we wish to retrieve the actual method object rather than the result of evaluation, we place the expression in a quoted block.
foo := { putln "It works!". }.
#'(foo).
;; No output
Up until this point, the only methods we have written have taken zero arguments. Naturally, with message passing being the primary means of communication in Latitude, we would like to be able to pass arguments with a message.
The first “argument” to a method is called self
, and it always
refers to the recipient of the message.
foo := Object clone.
foo bar := { self. }.
foo bar == foo. ; ==> True
There are several syntaxes for passing other arguments. We’ve already used most of them without calling attention to it. Arguments can be enclosed in parentheses or separated from the method name by a colon. The following are equivalent.
foo bar (1, 2, 3, 4).
foo bar: 1, 2, 3, 4.
The latter form is most often used when a statement consists of a
single expression which has side effects. For example, so far we’ve
been using println
with the parenthesized form, as that is more
familiar to users coming from other languages, but it is more common
to see println
used with a colon to indicate the side effect of
printing.
println: someValue.
The primary benefit of this is that :
has a very low syntactic
precedence, so operators like string concatenation (++
) can be used
without causing issues.
println: "The value of someValue is " ++ someValue.
Additionally, as an exception to the above rule, if there are no
arguments to a method, the parentheses may be omitted entirely.
Finally, if there is a single argument and it is a literal, such as
1
or "ABC"
, it need not be enclosed in parentheses or separated by
a colon. Parentheses may also be omitted if the method’s name is an
operator, such as +
or *
.
Below is a summary of the rules for how to pass arguments. It is not expected that you memorize these rules; they’re primarily built to maximize the intuitiveness of the call syntax.
Now that we know how to pass arguments, the next question is how to
receive them on the method side. Any arguments passed are stored in
the dynamic variables $1
, $2
, $3
, etc.
addTwoNumbers := { $1 + $2. }.
addTwoNumbers (3, 4). ; ==> 7
An important side effect of this is that arguments, as dynamic variables, are always forwarded down the call stack. This makes it very easy to write a delegator function.
foo := { $1 + $2. }.
bar := { foo. }.
bar (3, 4). ; ==> bar
This especially comes in handy when designing an extension object, which forwards most of its methods calls to an inner object. It is never necessary to explicitly forward the arguments, as they are always implicitly passed onward.
Unless your method is exceedingly short (like our addTwoNumbers
above), you will likely be giving lexical names to these dynamic
variables, to make them self-documenting and also to prevent them from
getting corrupted in inner scopes. This pattern is so common that
Latitude provides automatic techniques for doing so.
First, note that all of the basic control flow techniques in Latitude take a method as an argument. We will discuss these techniques in detail in the next chapter, but it is worth noting that Latitude has no explicit notion of a “block”, so any time a control structure or similar method requires user-customizable behavior, that method will take another method as an argument. As such, it is fairly common to end up nested several methods deep.
First, since the name self
is bound every time a method is called,
it is difficult to access the self
value from several layers out.
someMethod := {
if (blah) then {
someBehavior {
;; Would like to access someMethod's self
parent parent self. ; Ugly, but works
}.
} else {
doNothing.
}.
}.
Since it is reasonably common to have several nested methods within
one another and only care about one of the self
values, Latitude
offers the localize
method. localize
is a method called on the
lexical scope which, when called, sets this
equal to self
. this
is not rebound at every scope, so if it is used in an inner scope, it
will still have the same value it did before.
someMethod := {
localize.
if (blah) then {
someBehavior {
;; Would like to access someMethod's self
this. ; self at the time of the localize call
}.
} else {
doNothing.
}.
}.
As for non-self
arguments, the primary concern with those is that,
being dynamically scoped, they can very easily be corrupted by inner
scopes.
someMethod := {
if (blah) then {
$1 toString.
} else {
doNothing.
}.
}.
The $1
in the if-statement does not refer to the first argument of
someMethod
. It likely refers to the { $1 toString. }
method
itself, the first argument of then
, but this is unspecified. As
such, for any nontrivial methods, it is desirable to convert these
dynamic argument variables into lexical names. This is done with the
takes
primitive.
someMethod := {
takes '[n].
if (blah) then {
n toString.
} else {
doNothing.
}.
}.
takes
takes an array of symbols and binds each of the arguments to
the corresponding (lexical) symbol. We haven’t discussed arrays or the
“literal array” syntax used in the example, but for now that syntax
can be regarded as a “special” part of the takes
call, and we can
learn what it actually is later.
takes '[var1, var2, var3].
It is relatively common, at the beginning of any substantial Latitude
method, to see a takes
call and a localize
call. By convention,
localize
and takes
should always be used at the very beginning of
a method, and, if both are present, localize
should be listed first.
This makes it easy to read off, at a glance, the number and names of
the formal arguments expected by the method, by looking for a takes
call at the top of the method.
Now you have a good idea of the way variables, both dynamic and lexical, work in Latitude, as well as how to pass and forward arguments to methods. In the next chapter, we’ll discuss some basic flow control tools.
[up]
[prev - The Basics]
[next - Simple Flow Control]
1 We use
println
rather than putln
here. putln
always prints a string,
verbatim, whereas println
calls toString
. We can’t do putln
because i
is a number, not a string