Like every other nontrivial language, Latitude provides methods for conditional branching, looping, and the like.
The simplest form of flow control, arguably not even flow control, is
the do block, which takes a single method and runs it,
unconditionally once.
do {
putln "This will be printed once".
}.
This sort of block is only really useful in situations where some intermediate local variables are needed, but you don’t want those variables polluting the enclosing scope.
foo := do {
a := intermediateValue1.
b := intermediateValue2.
;; ... Some complicated expression involving a and b
}.
;; Now a and b only exist inside the do-block and
;; don't pollute the outer scope
Before we start talking about conditionals, it would be prudent to
discuss what constitutes a true and false value. Latitude defines a
Boolean “type” and “instances” True and False. Remember that,
since Latitude is prototype-oriented, Boolean is simply an object,
from which the True and False values are cloned.
Now, any object can be used in a place where a conditional is
expected. When a conditional is evaluated, the toBool message is
sent to that object, with no arguments. The result of toBool must be
either the True or False object, and that result determines the
truthiness of the value.
Most built-in objects in Latitude are truthy. In particular, any
numbers, strings, symbols, and methods are truthy (this includes 0 and
“”). The only falsy values included in the core Latitude library (that
is, the portion of the standard library that does not require imports)
are the False value itself and the special Nil object.
Nil is a relatively simple object, cloned directly from
Object.1 It behaves as an “empty”
or “nullary” object, in the cases where you need to show that a slot
or a value is well-defined but empty. When checking whether an object
is Nil, prefer passing the nil? method rather than checking for
equality against the Nil object. The reason for this is that it is
possible for programmers to clone Nil and make their own nil-like
objects.
By convention, Latitude objects are truthy unless they represent
failure in some sense. False is falsy for obvious reasons. Nil
usually represents the return value of a failed lookup or a
computation that failed to produce a result, so it is also falsy. The
'unit-test module provides a FailedTest type, representing a
failed unit test, which is also falsy. In general, when defining your
own types, make them truthy unless you can specifically justify
falsehood.
Latitude objects will respond to the messages and, or, and not
in the logical ways. (a) and (b) returns a if it is falsy, or b
otherwise. (a) or (b) returns a if it is truthy, or b otherwise.
(a) not returns True if a is false and False if it is true. By
default, these operators do not short-circuit. Like most things in
Latitude, the Boolean operators are simply Latitude methods, so they
follow the normal Latitude argument passing rules. However, these
operators will call any methods they are passed, so if you wish to
enable short-circuit evaluation, simply pass methods instead of flat
values.
;; a and b will be evaluated immediately
(a) or (b).
;; b will only be evaluated if a is falsy
{ a. } or { b. }.
This paradigm is used frequently in Latitude to give the user of an API control over whether or not to stall evaluation of arguments.
One thing to be careful about is the “target” of these short methods.
Passing methods to other methods is incredibly common in Latitude,
and, strictly speaking, every method in Latitude has a self.
Sometimes, however, it is not entirely obvious what that self is.
For example, in the logical operator case, what do you think the
result of { self. } or { True. } will be?
% { self. } or { True. }.
Conditional
As it turns out, when a conditional is being evaluated, such as by a
logical operator or, later, by an if-statement, it will always be
called on a specialized Conditional object. That conditional object
is not really that useful. It’s truthy, so we see it as the result of
or rather than True, but there’s not a whole lot to be done on it.
The point to be made is that some care must be taken when using self
in an inner scope, as it is sometimes not immediately obvious what
self actually is. When in doubt, consult the documentation for the
method you’re calling.
After all that build-up, let’s discuss the most basic conditional: the if-statement. No language would be complete without it. In Latitude, an if-statement looks like this.2
if (conditional) then {
trueCase.
} else {
falseCase.
}.
This may look quite a bit like a special syntactic form, but it is
not. If-statements are methods like any other. In this case, we are
passing the if message to the global scope. The if message always
returns a conditional object, on which we then send the then message
(which returns the same object) and, subsequently, the else message.
However, we don’t have to worry about those details most of the time;
we just think of it as a single statement and move on.
The actual behavior of the if-statement is reasonably simple. If the
conditional form is truthy then the true case is returned; otherwise,
the false case is returned. The else block is required for this
form of conditional.
Latitude also provides single-branch suffix forms called ifTrue and
ifFalse, which behave in the way you might expect.
obj ifTrue { body. }.
obj ifFalse { body. }.
In the first case, the body is called if obj is truthy, and no
action is performed otherwise. In the second case, the body is called
if obj is falsy, and no action is performed otherwise. ifTrue and
ifFalse always return the object obj which was initially checked
for truthiness.
These suffix forms make it possible to write Smalltalk-style conditionals, provided the return value is not used. It would, however, be relatively unusual to see Latitude code written in this way.
obj ifTrue {
putln "It's true".
} ifFalse {
putln "It's false".
}.
In addition to the if-statement, Latitude provides a cond form that
should be familiar to Lisp coders.
cond {
when (cond1) do { body1. }.
when (cond2) do { body2. }.
else { body3. }.
}.
A cond form is the equivalent to a chain of if-else-if calls. The
first when which has a truthy conditional will have its body
evaluated and returned. else, in this context, is roughly equivalent
to when (True) do.
Finally, Latitude supports a case statement that behaves similarly
to the cond form.
case (obj) do {
when (test1) do { body1. }.
when (test2) do { body2. }.
else { body3. }.
}.
Like cond, the case statement will perform the body associated
with the first matching when form. For a statement of the form when
(test) do { body. }., the test test =~ obj will be performed to
determine whether the match was successful. =~ defaults to behaving
just like == but can be overridden. In particular, else is
equivalent to when (...) do, where ... is the global Ellipsis
object, for whom =~ always returns true.
Latitude has two basic looping “primitives”, and then several more
abstract constructs are built on top of them. The most basic looping
construct in Latitude is loop, which performs its body forever,
unconditionally. If you run the below snippet, just make sure you know
how to exit it. On Unix-like operating systems, an interrupt signal
(CTRL+C) will work.
loop {
putln: "This will never stop printing".
}.
However, more commonly, we would like our loops to terminate at some point. While it is possible to break from an infinite loop (we will see a very powerful construct in the next chapter that allows this), usually we want to terminate the loop normally when some condition is met. Latitude has a typical while-loop for those purposes.
while { condition. } do {
body.
}.
Note that the condition is enclosed in braces. This is because, like
all of these constructs, while is just a method in Latitude. If we
failed to put our condition in braces, then it would be evaluated
once, and the result of that evaluation would be passed to while,
whereas with braces the method body will be evaluated at every loop
iteration.
Latitude also provides several abstract looping constructs. There are too many to cover in this chapter, but here are a few of the basic ones.
10 times do { putln "This will print ten times.". }
1 upto 11 do { putln "This will also print ten times.". }.
11 downto 1 do { putln "This prints ten times as well.". }.
In all three cases, the current iteration will be passed as an
argument to the loop body and can therefore be accessed with $1.
10 times do {
putln: "Iteration number " ++ $1.
}.
Many of the other looping constructs rely on iterators and collections, which will be discussed later.
loop and while each have variants labeled, respectively, loop*
and while*. These variants behave like the original constructs except
that they also enable exiting the loop prematurely.
Specifically, two additional methods become available: next and
last. next takes no arguments and jumps back to the start of the
loop body. last takes a single argument and exits the loop, using
the argument as the loop’s return value.
loop* {
putln "This will print forever".
next.
putln "This one will never print".
}.
loop* {
putln "This will now only print once".
last (Nil).
}.
Keep in mind that the starred forms of the loops are slightly less efficient than their simpler equivalents. It shouldn’t make much of a difference, but it is ideal to omit the star unless the additional functionality is actually needed.
We’ve discussed the basic looping and conditional structures in Latitude. In the next chapter, we’ll discuss iterable collections and several of the looping constructs available on them.
[up]
[prev - Variables]
[next - Collections]
1 If you happen to
run Parents hierarchy (Nil) or Nil parent, you will likely notice
that there is in fact a third object in the inheritance hierarchy.
Nil is in fact cloned from Object; that is still true. The third
object you see there is a mixin object, which will be discussed later.
2 Sometimes, you will
see the conditional block enclosed in braces as a method as well, like
if { conditional. } then .... This works fine as well, since if
follows the same rules as or and and with regard to methods. In
fact, the trueCase and falseCase don’t even strictly have to be
methods, but then both would be evaluated and (in most cases) that
would defeat the purpose of the if-statement.