4. Flow Control

4.1 Comparisons

Flow control is the art of adding logic to your script. The constructs presented here will allow your programs to make decisions based on conditions. There are even constructs for iterating through the various Sleep data structures. I will start with the if-statement.

If/else statements let you compare different values and execute a certain part of the script based on the result of the comparison.

if (v1 operator v2) { # .. code to execute .. } else if (-operator v3) { # .. more code to execute .. } else { # do this if nothing above it is true }

An if-statement checks a predicate expression. Sleep executes the next block of code if the expression is true. Sleep supports binary and unary predicate expressions. A predicate expression asks a question about data. For example the eq predicate asks: "Are these two strings equal?" After the if-statement any number of optional if-else blocks may follow. Sleep checks these in order. The interpreter stops checking once one of the if-statements in the chain succeeds. Sleep defaults to the else block after all other if-statements fail.

These predicate operators compare numbers.

Operator Description
== equal to
!= not equal to
< less than
> greater than
<= less than or equal to
>= greater than or equal to

These predicate operators compare strings.

Operator Description
eq equal to
ne not equal to
lt less than
gt greater than
isin is substring v1 contained in string v2
iswm is string v1 a wildcard match of string v2

Why does sleep have separate comparisons for strings and numbers?

Sleep tries to fit values to the context of an operator. For example, the numerical comparison == expects two numbers. This operator will cast anything it sees into a number. The eq comparison expects two strings. As a scripter, you need to be aware of what comparison is taking place. For example the string "3" and the double 3.0 are equivalent with ==. When you compare these values as strings, "3" and "3.0" are not the same string. For these reasons Sleep has separate comparisons for strings and numbers.

This last set of predicates compares scalars by identity and reference.

Operator Description
=~ is v1 equal to v2 under the rules of scalar identity (3.2 Fun with Arrays)
is compare the object reference v1 to the object reference v2

Example: Guessing Game

This example showcases all the skills up to this point in the chapter. It is the ever-classic guessing game. Simply tell the computer what number it is thinking of to win.

# most people think of the number 4 or 7 when asked # to think of something 1-10... this program will to. $number = rand(@(4, 7)); # read in a number print("What number am I thinking of? "); $guess = readln(); # check the guess. if ($guess == $number) { println("You got it!! You must be psychic"); } else if ($guess < $number) { println("Too low... you lose!"); } else if ($guess > $number) { println("Too high, you still lose"); } println("The number was $number $+ , thanks for playing");

$ java -jar sleep.jar guess.sl What number am I thinking of? 2 Too low... you lose! The number was 7, thanks for playing $ java -jar sleep.jar guess.sl What number am I thinking of? 4 You got it!! You must be psychic The number was 4, thanks for playing

This example uses an if, else-if, else-if chain. This program gets the guess with &readln. The &readln function returns a string. This is a nonissue as the number comparisons convert the string to an integer.

Combining Comparisons

Combine comparisons with the logic operators && (AND) and || (OR). These operators evaluate each predicate condition in order.

sub check { if (($1 > 0) && ($1 <= 10)) { println("$1 is within 0 .. 10"); } else { println("$1 is out of range!!"); } } check(5); check(88888888);

5 is within 0 .. 10 88888888 is out of range!!

This example defines a subroutine to check if a value is within the range of 1 to 10.

Sleep short circuits comparisons. With AND, if $1 > 0 is false, then Sleep never evaluates the next statement $1 <= 10 as the whole comparison is already false. With OR, if one of the arguments is true, then the whole expression is already true and evaluation stops.

To avoid confusing the Sleep parser it "helps" to put parentheses around the predicate expressions. For example: (a comparison) && (another comparison) && (who cares). The parser does not always need these parentheses but they may make your code cleaner.

You can use as many && and || to create logical chains of comparisons as complex as you like. The comparison ($x == 3) || (($x < 0) && ($x > 10)) evaluates to true for any $x value equivalent to three or less than zero and greater than ten.

Negation

You can negate most comparisons by inserting an exclamation point before the predicate operator.

if ("I" !isin "Team") { println("There is no 'I' in Team"); } if ("m" isin "Team" && "e" isin "Team") { println("... but there is an m and an e"); }

There is no 'I' in Team ... but there is an m and an e

This example checks the string "Team" does not contain the substring "I". Of course I can't mention there is no "I" in "Team" without elluding to the fact the letters to form the word "me" are present.

Unary Predicates

Until now all the predicates have been binary predicates. Sleep supports unary (one argument) predicates as well. Unary predicates always begin with a dash character.

Operator Description
-isarray is v1 a scalar array
-isfunction does v1 reference a function
-ishash is v1 a scalar hash
-isletter is v1 a letter
-isnumber is v1 a number
-isupper is v1 an upper case string
-islower is v1 a lower case string
-istrue is v1 "true"

Negate a unary predicate by prefixing the dash with an exclamantion point:

$ java -jar sleep.jar >> Welcome to the Sleep scripting language > ? -isnumber 3 true > ? !-isnumber 3 false > ? !-isnumber "Zebras" true

What is truth

Truth is all relative. Here, true means any value that is not the empty string, not zero, and not $null. Everything else is true.

Why does truth matter? Some comparisons do not contain a predicate expression. Sometimes you can use a scalar value by itself as the comparison for an if statement. A scalar that meets the true value rules will evaluate to true.

sub check { if ($1) { println("' $+ $1 $+ ' is true!"); } else { println("' $+ $1 $+ ' is false :("); } } check("the truth!"); check(0); check(""); check("0");

'the truth!' is true! '0' is false :( '' is false :( '0' is false :(

The Conditional Operator

Sleep supports the conditional operator iff. The first parameter to iff is a predicate expression. The conditional operator short circuits arguments. Sleep evaluates and returns the second argument when the predicate expression is true. Sleep will evaluate and return the third argument when the predicate expression is false. If you neglect to provide a second or third argument the iff function will use 1 and $null.

sub showTime { $time = int((ticks() - $time) / 1000); println($time . " second" . iff($time != 1, "s")); } $time = ticks(); # do some sort of intensive operation here... sleep(3600); showTime();

3 seconds

The conditional operator simplifies the output for this example.

Debugging Comparisons

Sometimes when programming it helps to know what decisions your program is making, with what information, and to what outcome. Many times programmers do a poor man's debug by using &println statements all over the place to figure out what is happening. Sleep's built-in &debug function includes a mode to trace all logical comparisons. This mode can help you track down potential logic issues. Set this flag with debug(debug() | 64).

debug(debug() | 64); # enable DEBUG_TRACE_LOGIC $favorite = "chocolate"; if ($favorite == "mint chocolate") { println("I like mint chocolate chip!"); }

Trace: 'chocolate' == 'mint chocolate' ? TRUE at tracelogic.sl:5 I like mint chocolate chip!

From the example above you may notice a counterintuitive result. The logic traceing shows that two separate strings are equal to eachother with the == predicate. This should offer an indiciation that something is wrong (using == instead of eq for strings is a common mistake). With this functionality you will know to look at the == operator more closely to figure out what is happening. You may recall that == is for numbers. Both strings converted to numbers evaluate to 0.

4.2 Loops

Loops are a mechanism for counting bottles of beer programatically. A loop executes while a condition holds true. Sleep has four looping constructs. I cover each of these constructs in this section.

While Loops

A while loop executes a series of statements while a comparison evaluates to true.

The syntax for a while loop is:

while (comparison) { code }

Now I'd like to demonstrate "99 Bottles of Beer?" with a while loop.

# 99 Bottles of Beer on the Wall # while loop version. $total = 99; while ($total > 0) { $beer = "$total bottle" . iff($total == 1, "", "s"); println("$beer of beer on the wall, $beer of beer."); $total = $total - 1; if ($total == 0) { println("Take one down and pass it around, no more bottles of beer on the wall"); } else { $beer = "$total bottle" . iff($total == 1, "", "s"); println("Take one down and pass it around, $beer of beer on the wall."); } println(); } println("No more bottles of beer on the wall, no more bottles of beer."); println("Go to the store and buy some more, 99 bottles of beer on the wall.");

While loops provide the most flexibility over how the loop works.

For Loops

For loops are similar. The for loop consolidates the initialization, comparison, and increment statements into one statement.

for (initialization; comparison; increment) { code }

For loops allow empty statements in the intialization and increment fields. These fields also allow multiple statements. Separate statements with a comma.

The break and continue commands affect the body of the for loop. The increment step still executes. This is one of the key differences between a for loop and a while loop.

More beer?

for ($total = 99; $total > 0; $total--, $beer = "$total bottle") { $beer = $beer . iff($total == 1, "", "s"); println("$beer of beer on the wall, $beer of beer."); if (($total - 1) == 0) { println("Take one down and pass it around, no more bottles of beer on the wall."); } else { $beer = ($total - 1) . " bottle" . iff($total == 1, "", "s"); println("Take one down and pass it around, $beer of beer on the wall."); } println(); } println("No more bottles of beer on the wall, no more bottles of beer."); println("Go to the store and buy some more, 99 bottles of beer on the wall.");

Use for loops when you can isolate your initialization and increment statements from the code.

Assignment Loops

An assignment loop executes a block of code while an expression evaluates to a non-$null value. The assignment loop evaluates expression and assigns to the variable each iteration.

while variable (expression) { code }

Assignment loops are great at reading files line by line.

$handle = openf("readfile.sl"); while $read (readln($handle)) { # do something useful. }

Use assignment loops when you want to evaluate an expression and operate on its value until it is $null.

Foreach Loops

Similar to the assignment loop is the foreach loop. Foreach loops take either a function, an array, or a hash scalar and iterate over each item.

Foreach loops note the index and value of each iteration. For arrays and functions the index is a counter that starts at zero. The hash scalar uses the dictionary key as the index. In this form the value is the data at the current index.

foreach index => value (source) { code }

Optionally the foreach loop can assign just the value of each iteration. In the case of a hash, this form will provide the dictionary key.

foreach value (source) { code }

Changes to the value will change the value in the source.

%data = %(foo => "a", bar => "b", baz => "c", jaz => "d"); foreach $key => $value (%data) { if ($value eq "c") { $value = "coolios"; } } println(%data);

%(foo => 'a', baz => 'coolios', bar => 'b', jaz => 'd')

Avoid functions that change the structure of the source during foreach loops. This means &push, &pop, and friends against the source are off-limits during a foreach loop.

You may remove the current value during a foreach loop. Use the &remove function with no arguments.

@data = @(8, 7, 6, 5, 4, 3, 2, 1, 0); foreach $index => $value (@data) { if ($index == $value) { println("$index == $value !!!"); remove(); } } println(@data);

4 == 4 !!! @(8, 7, 6, 5, 3, 2, 1, 0)

The foreach loop calls a function each iteration to iterate over it. The foreach loop terminates when the function returns $null.

sub range { # Returns a new function that returns the next number in the # range with each call. Returns $null at the end of the range # Don't worry, closures will come in the next chapter :) return lambda( { return iff($begin <= $end, $begin++ - 1, $null); }, $begin => $1, $end => $2); } foreach $value (range(8, 13)) { println($value); }

8 9 10 11 12 13

Sleep foreach loops are also able to iterate over an object scalar containing a java.util.Iterator object.

Foreach loops are useful for iterating over a data structure. The foreach loop is ideal for iterating over arrays. Each iteration walks one element of the linked list providing the best performance.

Loop Control

Use break and continue to alter the flow of control within a loop.

The break command immediately terminates the current loop.

for ($x = 0; $x < 10; $x++) { if ($x == 3) { break; } println($x); } println("done!");

0 1 2 done!

The continue command causes an immediate jump to the next iteration of the current loop.

for ($x = 0; $x < 10; $x++) { if ($x > 2 && $x < 8) { continue; } println($x); } println("done!");

0 1 2 8 9 done!

4.3 Exceptions

Sleep supports the exception paradigm for error management. Sometimes an error occurs that you don't want to handle in your code. This error is an exceptional condition. You can send this condition through all calling functions with the throw command.

Use the throw command to stop the current function and send the condition up to the first exception handler. Sleep ignores the throw command when the thrown value is $null.

sub multiplyBy3 { if (!-isnumber $1) { throw "&multipyBy3( $+ $1 $+ ) requires a number!"; } return $1 * 3; }

This subroutine checks if an argument is a number. If your program calls multiplyBy3("hi") then the exception "&mulityBy3(hi) requires a number!" is sent up the call stack. Sleep searches the calling functions for an exception handler. When one is found the handler executes. An exception allows scripts to jump out of the current execution context to deal with an error. An uncaught exception will cause the script to exit.

Sleep exceptions don't bubble up to the Java runtime environment. The only exception to this is when an exception is thrown within a proxy instance. 7.2 Parameter Marshalling: Proxy Instances covers them in detail.

Now we will look at how to install a handler and catch exceptions.

try { multiplyBy3("hi"); } catch $message { warn("Failed to multiply by 3: $message"); printAll(getStackTrace()); }

Warning: Failed to multiply by 3: &multipyBy3(hi) requires a number! at eval:6 eval:2 &multiplyBy3() eval:4 <origin of exception>

This example installs an exception handler with the try-catch block. The try block executes code that may result in an exception. Sleep calls the catch block when any exception occurs. Exceptions bubble up. So if you call a function that calls a function that throws an exception and no other try-catch block is in place, your catch block will receive the exception.

The catch block assigns the exception to the variable specified with the catch keyword. In this example the exception is available as $message.

The function &getStackTrace returns the call stack at the time of the exception. Use this to deduce your programs behavior.

Use the &warn function to communicate an error message to Sleep. This function is more generic than &println. It will work regardless of where Sleep is embedded. &warn also outputs the current script name and line number.

Choice in Error Handling

Sleep does not use exceptions as the default error mechanism. Soft errors are available with the &checkError function. A soft error is a non-fatal error. An error resulting from the failure to open a file is a soft error. The &checkError function returns and clears the most recent error. As a side note debug(debug() | 2) will print a notification when an error is available to &checkError.

$handle = openf("doesNotExist"); if (checkError($error)) { warn("error: $error"); return; } println("file opened!");

Warning: error: java.io.FileNotFoundException: /Users/raffi/manual/manual/doesNotExist (No such file or directory) at choice0.sl:5

You can throw individual errors yourself. Insert throw checkError($error); in your script to throw an error if there is one. &checkError will return $null when there is no error. You may recall that throw will not act on $null values.

try { $handle = openf("doesNotExist"); throw checkError($error); println("file opened!"); } catch $exception { warn("error: $exception"); }

Warning: error: java.io.FileNotFoundException: /Users/raffi/manual/manual/doesNotExist (No such file or directory) at choice1.sl:10

Sleep supports a debug option to throw all errors. You can enable this option with debug(debug() | 34). This debug option will immediately throw an exception as soon as an error is made available.

debug(34); try { $handle = openf("doesNotExist"); # la la la... do stuff... println("file opened!"); } catch $exception { warn("error: $exception"); }

Warning: error: java.io.FileNotFoundException: /Users/raffi/manual/manual/doesNotExist (No such file or directory) at choice2.sl:12

The &checkError mechanism for handling errors is simple. It requires little code on your part. It also supports automatic reporting through debug level 2. The try-catch mechanism requires more code. However the try-catch mechanism gives you a stack trace and allows you to deal with errors and recovery in one place. When writing applications in Sleep I use the try-catch mechanism. With this mechanism I feel comfortable that my program will better recover from unforeseen circumstances. When writing quick hacks (which I am known to do), I simply enable debug level 2. Ultimately the choice is yours.

4.4 Assertions

Assertions are a simple script debug mechanism. An assertion statement is a guard that makes sure a condition is true. Sleep will exit with an error message if the condition isn't true. The syntax for an assertion is:

assert condition;

You can attach a message to an assertion. This message is shown when the condition fails:

assert condition : message;

Assertions are great for last minute sanity checks. Can you tell what is wrong with this code snippet?

sub fact { return iff($1 == 0, 1, $1 * fact($1 - 1)); }

The factorial function expects a positive number. A negative number would cause this script to go into an infinite loop. Assertions enforce these expectations (sometimes called preconditions).

sub fact { assert $1 >= 0 : "invalid arg for fact: $1"; return iff($1 == 0, 1, $1 * fact($1 - 1)); } println(fact(-10));

Warning: invalid arg for fact: -10 at badfact2.sl:3

One reason programmers shy away from extra guards is performance fears. Sleep enables the assertions feature by default. Set the sleep.assert property to false to disable assertions. Chapter 1 discusses how to do this. The parser ignores all assert statements when assertions are off.

# this program will quit early when # assertions are enabled assert 2 + 2 : "eh, assertions are enabled"; println("Whee...");

$ java -jar sleep.jar compare.sl Warning: eh, assertions are enabled at compare.sl:4 $ java -Dsleep.assert=false -jar sleep.jar compare.sl Whee...

Take care that your assertion statements are free of side effect. A side effect is some change to the current state of the script. Assertions with side effects are a potential source of subtle bugs when disabled.