(archive 'newLISPer)

February 18, 2006

Control the flow

Filed under: newLISP — newlisper @ 21:09
Tags:

>

In many ways, newLISP is a small and compact language, but a closer look reveals many different ways to control the flow of your code. If you’ve used other scripting languages you’ll probably find your favourites here, and many more besides. Here’s a complete-ish – and rather long – list, in another extract taken from the newLISP book that I’m struggling to write in my odd moments.

All the control flow functions obey the standard rules of newLISP. The general form of each is usually a list in which the first element is a type of keyword, possibly followed by the local variables (if any), followed by one or sometimes more expressions to be evaluated.

Tests

Perhaps the simplest control structure you can write in any language is a simple if list, consisting of a test and an action:

(if button-pressed? (launch-missile))

The second expression, (launch-missile), is evaluated only if the first test, button-pressed? evaluates to ‘true’. 1 is true. 0 is true. “yes” is true. “spam” is true. In newLISP, most things are true. But there are a few important things that are false rather than true: nil, false, and the empty list ().

(if 1 (launch-missile))
;-> missiles launched, because 1 is true
(if 0 (launch-missile))
;-> missiles launched, because 0 is true
(if nil (launch-missile))
;-> nil, because nil is false
(if false (launch-missile))
;-> nil, because false is false
(if '() (launch-missile))
;-> (), and the missiles aren't launched

You can use as a test anything that evaluates to either true or false:

(if (> 4 3) (launch-missile))
;-> true, so the missiles are launched
(if (> 4 3) (println "4 is bigger than 3"))
;-> "4 is bigger than 3"

You can add a third expression, which is the ‘else’ action. If the first expression evaluates to nil or (), the third expression is evaluated, rather than the second, which is ignored:

(if 1 (launch-missile) (cancel-alert))
;-> launches the missiles
(if nil (launch-missile) (cancel-alert))
;-> alert is cancelled
(if false (launch-missile) (cancel-alert))
;-> alert is cancelled

Here’s a typical, real-world, three-part if statement, formatted to show the structure as clearly as possible:

(if (and socket (net-confirm-request))
    (net-flush)
    (finish "could not connect"))

Notice that although the (net-flush) and (finish) functions appear on adjacent lines, they’ll never both be evaluated together: it’s definitely either one or the other. This construction, with the lack of the familiar ‘signpost’ words such as then and else that you find in other languages, can catch you out if you’re not concentrating!

To have some actions carried out after a test returns false rather than true, you can use unless, which is if‘s pessimistic younger brother:

(unless disk-is-full (copy-directory))

and copy-directory will be called only if disk-is-full returns false. The advantage of using unless is that you don’t have to negate a test with not to reverse its sense. Without unless you would have to write this, which isn’t as easy to read:

(if (not disk-is-full) (copy-directory))

You can also use if with an unlimited number of tests and actions. In this case the if list consists of a series of test-action pairs. newLISP works through the pairs until one of the test succeeds, then executes that test’s corresponding action. If you can, format the list in columns to make the structure more apparent:

(if
    (< x 0)     (set 'a "impossible")
    (< x 10)    (set 'a "small")
    (< x 20)    (set 'a "medium")
    (>= x 20)   (set 'a "large")
    )

(If you’ve used other Lisp dialects, you’ll recognise that this is a better designed and simpler version of the cond conditional function. newLISP also lets you use the traditional cond structure if you prefer.)

You might be wondering how to do two or more actions if a test is successful or not. You’ll have to construct a group of expressions that form a single block for use in an if list. We’ll discuss how to do this in a minute.

Looping

Sometimes you want to repeat a series of actions more than once, going round in a loop. There are various possibilities. You might want to do the actions:

  • on every item in a list
  • a certain number of times
  • until something happens
  • while some condition prevails

newLISP has a solution for all of these, and more.

Every item in a list

Lisp programmers love lists, so dolist is a most useful function that sets a local loop variable to each item of a list in turn, and runs the series of actions on each. Put the name for the loop variable and the list in parentheses after dolist, then follow it with the actions. In the following example, we use another variable counter as well, before defining a local variable i to step through the results list of the sequence function:

(set 'counter 1)
(dolist (i (sequence -5 5))
    (println "Element " counter ": " i)
    (inc 'counter))
;->
Element 1: -5
Element 2: -4
Element 3: -3
Element 4: -2
Element 5: -1
Element 6: 0
Element 7: 1
Element 8: 2
Element 9: 3
Element 10: 4
Element 11: 5

Notice that, unlike if, the dolist function and many other control words let you write a series of expressions one after the other: here both the println and the inc function are called for each element of the list returned by sequence.

In some situations, you might prefer to use the mapping function map for processing a list. map can be used to apply a function (either a pre-defined one or a throwaway definition) to every element in a list, without using a local variable. For example, let’s use map to produce the same output as the above dolist function. We define a temporary ‘print and increase’ function and apply it to each element of the list produced by sequence:

(set 'counter 1)
(map (fn (i)
        (println "Element " counter ": " i)
        (inc 'counter))
    (sequence -5 5))

(Old Lispers note: fn is a synonym for lambda – use lambda if you prefer.)

You might also find flat useful for working through lists, because it flattens out lists with nested lists for easier processing, turning ((1 2 3) (4 5 6)) into (1 2 3 4 5 6).

A certain number of times

If you want to do something a fixed number of times, there’s dotimes and for. dotimes does a given number of repeats of the actions in the body of the list, and you should provide a name for the local variable, just like dolist:

(dotimes (c 10)
    (print c " times 3 is ")
    (println (* c 3)))
;->
0 times 3 is 0
1 times 3 is 3
2 times 3 is 6
3 times 3 is 9
4 times 3 is 12
5 times 3 is 15
6 times 3 is 18
7 times 3 is 21
8 times 3 is 24
9 times 3 is 27

Notice that counting starts at 0 and continues to ‘n – 1’. Programmers think this is sensible and logical; non-programmers just have to get used to starting their counting at 0.

You have to have a local variable with a lot of these constructs. Even if you don’t use it, you have to define one.

Use dotimes when you know the number of repetitions, but use for when you want newLISP to work out how many repetitions are needed, given start, end, and step values:

(for (c 1 -1 .5)
    (println c))
;->
1
0.5
0
-0.5
-1

newLISP is smart enough to work out that I wanted to step down from 1 to -1 by 0.5.

Until something happens, or while something is true

You might have a test for a situation that returns nil or () when something interesting happens, but otherwise returns a ‘true’ value, which you’re not interested in. To repeatedly carry out a series of actions until the test fails, use until or do-until:

(until (disk-full)
    (println "Adding another file")
    (add-file)
    (inc 'count))
(do-until (disk-full)
    (println "Adding another file")
    (add-file)
    (inc 'count))

The difference between these two is to do with when the test is carried out. In until, the test is made first, then the actions in the body are evaluated if the test fails. In do-until, the actions in the body are evaluated first, before the test is made, then the test is made to see if another loop is possible.

Which of those two fragments of code is correct? Well, the first one tests the capacity of the disk before adding a file, but the second one, using do-until, doesn’t check for free disk space until the file is added, which isn’t so cautious.

Just as unless is the pessimistic version of if, so until and do-until are the pessimistic versions of while and do-while.

(while (disk-has-space)
    (println "Adding another file")
    (add-file)
    (inc 'count))
(do-while (disk-has-space)
    (println "Adding another file")
    (add-file)
    (inc 'count))

Again, choose while if your test returns true, but choose until to do them if it returns nil. Choose the do variants of each to do the actions before the test.

Sequences of actions

A lot of newLISP control functions let you construct a ‘body’ of actions: a group of expressions that are evaluated one by one. Construction is implicit: you don’t have to do anything except write them in the right order and in the right place. Look at the while and until examples above: each has three expressions to be evaluated one after the other.

However, you can also create bodies of expressions explicitly using the begin, or, and and functions.

begin is a way of explicitly grouping expressions together in a single list. Each expression is evaluated in turn:

(begin
    (switch-on)
    (engage-thrusters)
    (look-in-mirror)
    (press-accelerator-pedal)
    ...)

and so on. You have to use begin only when newLISP is expecting a single expression. A frequent use for begin is with if constructions. Suppose you want to do two or more things if a test is successful, such as print a number and then increment it. Don’t write this:

(if (number? x)
    (println x " is a number ") ; first thing to do
    (inc 'x)) ; you think this is the second thing? Wrong!

because the third element of an if list is the ‘else’ part, only to be evaluated when the test fails. Instead, use begin to ‘group’ the two expressions together into a single block:

(if (number? x)
    (begin
        (println x " is a number ")
        (inc 'x)))

This is now a two part if expression; a test followed by one group of expressions, with no ‘else’ part.

What about the values returned by each expression in a block? In the case of begin, they’re thrown away. But for two other ‘group’ functions, and and or, the return values are important and useful.

The and function works through a list of expressions, like begin, but finishes if one of them returns nil (or false). To get to the end of the and list, every single expression has to return some true value. If one expression ‘fails’, evaluation stops and the most recent successful value is returned. newLISP ignores the rest of the expressions in the list.

Here’s an example of and that tests whether disk-item contains a useful directory:

(and
    (directory? disk-item)
    (!= disk-item ".")
    (!= disk-item "..")
    ; here if all tests succeeded
    )

The disk item has to ‘pass’ all three tests: it must be a directory, it mustn’t be the “.” directory, and it mustn’t be the “..” directory (this is Unix-speak, by the way). When it successfully gets past these three tests, expression evaluation continues.

You can use and for numeric expressions too:

(and
    (< c 256)
    (> c 32)
    (!= c 48))

which tests whether c is between 33 and 255 inclusive, and not equal to 48.

or is more easily pleased than its opposite and. The series of expressions are evaluated until one returns a true value. The rest are then ignored. You could use this to work through a list of important conditions, where any one ‘failure’ is important enough to abandon the whole enterprise. Or, conversely, use or to work through a list where any one success is good enough to continue. Whatever, remember that as soon as newLISP gets a non-nil result, the or function completes.

You may or may not find a good use for amb – the ambiguous function. Given a series of expressions in a list, amb will choose and evaluate just one of them. If we call amb 20 times, we’ll get a different result each time.

(dotimes (x 20)
    (amb
        (println "Will it be me?")
        (println "It could be me!")
        (println "Or it might be me...")))
;->
Will it be me?
It could be me!
It could be me!
Will it be me?
It could be me!
Will it be me?
Or it might be me...
It could be me!
Will it be me?
Will it be me?
It could be me!
It could be me!
Will it be me?
Or it might be me...
It could be me!
...

Selection

To test for a series of alternative values, you can either use if or case. The case function lets you execute expressions based on the value of a ‘switching’ expression. It consists of a series of value/expression pairs:

(case n
    (1 (println "un"))
    (2 (println "deux"))
    (3 (println "trois"))
    (4 (println "quatre"))
    (true (println "je ne sais quoi")))

newLISP works through the pairs in turn, seeing if n matches any of the values 1, 2, 3, or 4. At the first match, the matching expression is evaluated and the case function finishes, returning the value of the expression. It’s usual to put a final pair with true and a general purpose expression to handle failure. n will always be true, so put this at the end.

Notice that the potential match values are not evaluated. This means that you can’t write this:

(case n
    ((- 2 1) (println "un"))
    ((+ 2 0) (println "deux"))
    ((- 6 3) (println "trois"))
    ((/ 16 4) (println "quatre"))
    (true (println "je ne sais quoi")))

even though this ought logically to work – if n is 1, you would expect it to match the first expression (- 2 1). In this example, only the true expression’s action is evaluated.

Earlier I mentioned that cond is an old-style version of if. For the record, a cond statement in newLISP looks like this:

(cond
    ((< x 0)    (set 'a "impossible"))
    ((< x 10)   (set 'a "small"))
    ((< x 20)   (set 'a "medium"))
    ((>= x 20)  (set 'a "large"))
    )

which is essentially the same as the if version, except that each pair of test-actions has to be enclosed in parentheses. Here’s the if version for comparison:

(if
    (< x 0)     (set 'a "impossible")
    (< x 10)    (set 'a "small")
    (< x 20)    (set 'a "medium")
    (>= x 20)   (set 'a "large")
    )

Variables as well

Many of the control functions we’ve met also involve the definition of local variables, which survive for the duration of the function, then disappear. The let and letn functions combine this feature with the ability to define variables. The first item in the let list is a list containing variables and expressions which initialize each variable. The remaining items in the list are expressions that can access those variables. It’s a good idea to line up the variable/starting value pairs clearly:

(let
    (x (* 2 2)
     y (* 3 3)
     z (* 4 4)) ; end of variable initialization
     (println x)
     (println y)
     (println z))
 ;->
 4
 9
 16

This example creates three local variables, x, y, and z, and assigns values to each. The body contains three println expressions. After these finish, the values of x, y, and z are no longer accessible.

You don’t have to initialize the variables:

(let
    (x y z)
     (println x)
     (println y)
     (println z))
;->
nil
nil
nil

If you want to refer to a local variable elsewhere in the same initialization section, use letn rather than let:

(letn
    (x 2
     y (pow x 3)
     z (pow x 4))
     (println x)
     (println y)
     (println z))

In the definition of y, we referred to the value of x, which we’d only just defined to be 2. The ‘nested’ version of let, letn, allows us to do this.

And finally

newLISP has many more ways to control the flow of code execution. Using newLISP macros, you can write your own, and use them in the same way that you use the built-in functions. This definitely belongs in another chapter, though!

Advertisements

4 Comments »

  1. >Nice! I finally understand differences between while, do-while and until, do-until :)I believe that there is no ‘false’ in newLISP. It works like ‘nil’ only because it hasn’t been initialized:> falsenil> (set ‘false 10)10> (if false “true” “false”)”true”Also I believe that you always have to initialize the variables in ‘let’ or ‘letn’:> (let (x y z) (println x) (println y) (println z))nil [= x]nil [= y]nil [= z]nil [return value of ‘let’]> (set ‘y 3)3> (let (x y z) (println x) (println y) (println z))33nilnil> y3> (let (x y z) (println x) (println y) (set ‘y 8) (println z))33nilnil> y8=>x is local, initialized to ‘y’y isn’t local, but global – changed to 8z is probably implicitly set to ‘nil’

    Comment by Fanda — February 19, 2006 @ 16:27 | Reply

  2. >I implore you to only use let when necessary.IMHO it unnecessarily complicates things.Most of the time I see let used immediately after a define. There is a much more readable way to do that.Compare:=====(define (foo x y) (let ((a (+ x y)) (b (- x y))) (println a) (println b) ))=====(define (foo x y , a b) (setq a (+ x y) b (- x y)) (println a) (println b))=====The second seems much cleaner w/o the paren pollution.But after previewing this post I see that I can’t use

     or  tags. So this will look much different in this comment than it looks in vi.

    Comment by sarken — February 23, 2006 @ 16:25 | Reply

  3. >Thanks for the comments, folks! I’ll get round to updating the original post sometime soon.I also forgot “silent”, “catch” and “throw”, and a few other things… They’ll make it in one day!

    Comment by newlisper — February 25, 2006 @ 19:34 | Reply

  4. >Sorry sarken, I disagree. I can call your version of ‘foo’ like this ‘(foo 1 2 3)’ and bind ‘,’ to ‘3’! Yuck! It’s just so sloppy! :-)Don’t worry too much about all the parentheses (“paren pollution”) — as you program more Lisp they will start to disappear. (I am not kidding.)

    Comment by Rick Hanson — April 30, 2006 @ 21:22 | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: