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.