Introduction to newLISP/Controlling the flow
Controlling the flow
[edit | edit source]There are 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.
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 keyword followed by one or more expressions to be evaluated:
(keyword expression1 expression2 expression3 ...)
Tests: if...
[edit | edit source]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, a call to the launch-missile function, is evaluated only if the symbol button-pressed? evaluates to true. 1 is true. 0 is true - it's a number, after all. -1 is true. Most of the things that newLISP already knows about are true. There are two important things that newLISP knows are false rather than true: nil and the empty list (). And anything that newLISP doesn't know the value of is false.
(if x 1)
; if x is true, return the value 1
(if 1 (launch-missile))
; missiles are launched, because 1 is true
(if 0 (launch-missile))
; missiles are launched, because 0 is true
(if nil (launch-missile))
;-> nil, there's no launch, because nil is false
(if '() (launch-missile))
;-> (), and the missiles aren't launched
You can use anything that evaluates to either true or false as a test:
(if (> 4 3) (launch-missile))
;-> it's true that 4 > 3, so the missiles are launched
(if (> 4 3) (println "4 is bigger than 3"))
"4 is bigger than 3"
If a symbol evaluates to nil (perhaps because it doesn't exist or hasn't been assigned a value), newLISP considers it false and the test returns nil (because there was no alternative action provided):
(if snark (launch-missile))
;-> nil ; that symbol has no value
(if boojum (launch-missile))
;-> nil ; can't find a value for that symbol
(if untrue (launch-missile))
;-> nil ; can't find a value for that symbol either
(if false (launch-missile))
;-> nil
; never heard of it, and it doesn't have a value
You can add a third expression, which is the else action. If the test expression evaluates to nil or (), the third expression is evaluated, rather than the second, which is ignored:
(if x 1 2)
; if x is true, return 1, otherwise return 2
(if 1
(launch-missile)
(cancel-alert))
; missiles are launched
(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 function, formatted to show the structure as clearly as possible:
(if (and socket (net-confirm-request)) ; test
(net-flush) ; action when true
(finish "could not connect")) ; action when false
Although there are two expressions after the test - (net-flush) and (finish ...) - only one of them will be evaluated.
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! But you can easily put comments in.
You can 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 tests succeeds, then evaluates that test's corresponding action. If you can, format the list in columns to make the structure more apparent:
(if
(< x 0) (define a "impossible")
(< x 10) (define a "small")
(< x 20) (define a "medium")
(>= x 20) (define a "large")
)
If you've used other LISP dialects, you might recognise that this is a simple alternative to cond, the conditional function. newLISP provides the traditional cond structure as well. See Selection: if, cond, and case.
You might be wondering how to do two or more actions if a test is successful or not. There are two ways to do this. You can use when, which is like an if without an 'else' part.
(when (> x 0)
(define a "positive")
(define b "not zero")
(define c "not negative"))
Another way is to define a block of expressions that form a single expression that you can use in an if expression. I'll discuss how to do this shortly, in Blocks: groups of expressions.
Earlier, I said that when newLISP sees a list, it treats the first element as a function. I should also mention that it evaluates the first element first before applying it to the arguments:
(define x 1)
((if (< x 5) + *) 3 4) ; which function to use, + or *?
7 ; it added
Here, the first element of the expression, (if (< x 5) + *), returns an arithmetic operator depending on the results of a test comparing x with 5. So the whole expression is either an addition or multiplication, depending on the value of x.
(define x 10)
;-> 10
((if (< x 5) + *) 3 4)
12 ; it multiplied
This technique can help you write concise code. Instead of this:
(if (< x 5) (+ 3 4) (* 3 4))
you could write this:
((if (< x 5) + *) 3 4)
which evaluates like this:
((if (< x 5) + *) 3 4)
((if true + *) 3 4)
(+ 3 4)
7
Notice how each expression returns a value to the enclosing function. In newLISP every expression returns some value, even an if expression:
(define x (if flag 1 -1)) ; x is either 1 or -1
(define result
(if
(< x 0) "impossible"
(< x 10) "small"
(< x 20) "medium"
"large"))
The value of x depends on the value returned by if expression. Now the symbol result contains a string depending on the value of x.
Looping
[edit | edit source]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
- on every item in a string
- a certain number of times
- until something happens
- while some condition prevails
newLISP has a solution for all of these, and more.
Working through a list
[edit | edit source]newLISP programmers love lists, so dolist is a most useful function that sets a local loop symbol (variable) to each item of a list in turn, and runs a series of actions on each. Put the name for the loop variable and the list to be scanned, in parentheses, after dolist, then follow it with the actions.
In the following example, I set another symbol called counter as well, before defining a local loop variable i that will hold each value of the list of numbers generated by the sequence function:
(define counter 1)
(dolist (i (sequence -5 5))
(println "Element " counter ": " i)
(inc counter)) ; increment counter by 1
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 (increment) functions are called for each element of the list.
There's a useful shortcut for accessing a system-maintained loop counter. Just now I used a counter symbol, incremented each time through the loop, to keep track of how far we'd got in the list. However, newLISP automatically maintains a loop counter for you, in a system variable called $idx, so I can dispense with the counter symbol, and just retrieve the value of $idx each time through the loop:
(dolist (i (sequence -5 5))
(println "Element " $idx ": " i))
Element 0: -5 Element 1: -4 Element 2: -3 Element 3: -2 Element 4: -1 Element 5: 0 Element 6: 1 Element 7: 2 Element 8: 3 Element 9: 4 Element 10: 5
In some situations, you might prefer to use the mapping function map for processing a list (described later - see Apply and map: applying functions to lists). map can be used to apply a function (either an existing function or a temporary definition) to every element in a list, without going through the list using a local variable. For example, let's use map to produce the same output as the above dolist function. I define a temporary print and increase function consisting of two expressions, and apply this to each element of the list produced by sequence:
(define counter 1)
(map (fn (i)
(println "Element " counter ": " i)
(inc counter))
(sequence -5 5))
Experienced LISP programmers may be more familiar with lambda. fn is a synonym for lambda: use whichever one you prefer.
You might also find flat useful for working through lists, because it flattens out lists containing nested lists for easier processing, by copying:
((1 2 3) (4 5 6))
to
(1 2 3 4 5 6)
for example. See flat.
To work through the arguments supplied to a function, you can use the doargs function. See Arguments: args.
Working through a string
[edit | edit source]You can step through every character in a string using the equivalent of dolist, dostring.
(define alphabet "abcdefghijklmnopqrstuvwxyz")
(dostring (letter alphabet)
(print letter { }))
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
The numbers are the ASCII/Unicode codes.
A certain number of times
[edit | edit source]If you want to do something a fixed number of times, use dotimes or for. dotimes carries out a given number of repeats of the actions in the body of the list. You should provide a name for the local variable, just like you did with dolist:
(dotimes (c 10)
(println c " times 3 is " (* 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
You must supply a local variable with these forms. Even if you don't use it, you have to provide one.
Notice that counting starts at 0 and continues to n - 1, never actually reaching the specified value. Programmers think this is sensible and logical; non-programmers just have to get used to starting their counting at 0 and specifying 10 to get 0 to 9.
One way to remember this is to think about birthdays. You celebrate your 1st birthday when you complete your first year, during which time you were 0 years old. You complete your first 10 years of life when you start celebrating your 10th birthday, which starts when you stop being 9. The newLISP function first gets the element with index number 0...
Use dotimes when you know the number of repetitions, but use for when you want newLISP to work out how many repetitions should be made, given start, end, and step values:
(for (c 1 -1 .5)
(println c))
1 0.5 0 -0.5 -1
Here newLISP is smart enough to work out that I wanted to step down from 1 to -1 in steps of 0.5.
Just to remind you of this counting from 0 thing again, compare the following two functions:
(for (x 1 10) (println x))
1 ... 10
(dotimes (x 10) (println x))
0 ... 9
An escape route is available
[edit | edit source]for, dotimes, and dolist like to loop, carrying out a set of actions over and over again. Usually the repetition continues, inexorably, until the limit - the final number, or the last item in the list - is reached. But you can specify an emergency escape route in the form of a test to be carried out before the next loop starts. If this test returns true, the next loop isn't started, and the expression finishes earlier than usual. This provides you with a way of stopping before the official final iteration.
For example, suppose that you want to halve every number in a list, but (for some reason) wanted to stop if one of the numbers was odd. Compare the first and second versions of this dolist expression:
(define number-list '(100 300 500 701 900 1100 1300 1500))
; first version
(dolist (n number-list)
(println (/ n 2)))
50 150 250 350 450 550 650 750
; second version
(dolist (n number-list (!= (mod n 2) 0)) ; escape if true
(println (/ n 2)))
50 150 250
The second version stops looping if the test for n being odd, (!= (mod n 2) 0), returns true.
Notice the use of integer-only division here. I've used / rather than the floating-point division operator div as part of the example. Don't use one if you want the other!
You can supply escape route tests with for and dotimes too.
For more complex flow control, you can use catch and throw. throw passes an expression to the previous catch expression which completes and returns the value of the expression:
(catch
(for (i 0 9)
(if (= i 5) (throw (string "i was " i)))
(print i " ")))
The output is:
0 1 2 3 4
and the catch expression returns i was 5.
You can also devise flows using Boolean functions. See Blocks: groups of expressions.
Until something happens, or while something is true
[edit | edit source]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 counter))
(do-until (disk-full?)
(println "Adding another file")
(add-file)
(inc counter))
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.
while and do-while are the complementary opposites of until and do-until, repeating a block while a test expression remains true.
(while (disk-has-space)
(println "Adding another file")
(add-file)
(inc counter))
(do-while (disk-has-space)
(println "Adding another file")
(add-file)
(inc counter))
Choose the do- variants of each to do the action block before evaluating the test.
Blocks: groups of expressions
[edit | edit source]A lot of newLISP control functions let you construct a block of actions: a group of expressions that are evaluated one by one, one after the other. 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 that will be evaluated one after the other.
However, you can also create blocks of expressions explicitly using the begin, or, and and functions.
begin is useful when you want to explicitly group expressions together in a single list. Each expression is evaluated in turn:
(begin
(switch-on)
(engage-thrusters)
(look-in-mirror)
(press-accelerator-pedal)
(release-brake)
; ...
)
and so on. You normally use begin only when newLISP is expecting a single expression. You don't need it with dotimes or dolist constructions, because these already allow more than one expression.
It doesn't matter what the result of each expression in a block is, unless it's bad enough to stop the program altogether. Returning nil is OK:
(begin
(println "so far, so good")
(= 1 3) ; returns nil but I don't care
(println "not sure about that last result"))
so far, so good not sure about that last result!
The values returned by each expression in a block are, in the case of begin, thrown away; only the value of the last expression is returned, as the value of the entire block. But for two other block functions, and and or, the return values are important and useful.
and and or
[edit | edit source]The and function works through a block of expressions but finishes the block immediately if one of them returns nil (false). To get to the end of the and block, every single expression has to return a true value. If one expression fails, evaluation of the block stops, and newLISP ignores the remaining expressions, returning nil so that you know it didn't complete normally.
Here's an example of and that tests whether disk-item contains a useful directory:
(and
(directory? disk-item)
(!= disk-item ".")
(!= disk-item "..")
; I get here only if all tests succeeded
(println "it looks like a directory")
)
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 (Unix terminology). When it successfully gets past these three tests, evaluation continues, and the message was printed. If one of the tests failed, the block completes without printing a message.
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. This will always return either true or nil, depending on the value of c.
In some circumstances and can produce neater code than if. You can rewrite this example on the previous page:
(if (number? x)
(begin
(println x " is a number ")
(inc x)))
to use and instead:
(and
(number? x)
(println x " is a number ")
(inc x))
You could also use when here:
(when (number? x)
(println x " is a number ")
(inc x))
The or function is more easily pleased than its counterpart and. The series of expressions are evaluated one at a time 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 reason enough to continue. Whatever, remember that as soon as newLISP gets a non-nil result, the or function completes.
The following code sets up a series of conditions that each number must avoid fulfilling - just one true answer and it doesn't get printed:
(for (x -100 100)
(or
(< x 1) ; x mustn't be less than 1
(> x 50) ; or greater than 50
(> (mod x 3) 0) ; or leave a remainder when divided by 3
(> (mod x 2) 0) ; or when divided by 2
(> (mod x 7) 0) ; or when divided by 7
(println x)))
42 ; the ultimate and only answer
ambiguity: the amb function
[edit | edit source]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, but you don't know in advance which one:
> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
2
> (amb 1 2 3 4 5 6)
6
> (amb 1 2 3 4 5 6)
3
> (amb 1 2 3 4 5 6)
5
> (amb 1 2 3 4 5 6)
3
>...
Use it to choose alternative actions at random:
(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: if, cond, and case
[edit | edit source]To test for a series of alternative values, you can use if, cond, 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. As soon as a value matches, the expression is evaluated and the case function finishes, returning the value of the expression. It's a good idea to put a final pair with true and a catch-all expression to cope with no matches at all. n will always be true, if it's a number, so put this at the end.
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 the first expression (- 2 1) to match. But that expression hasn't been evaluated - none of the sums have. In this example, the true action (println "je ne sais quoi") is evaluated.
If you prefer to have a version of case that evaluates its arguments, that's easily done in newLISP. See Macros.
Earlier I mentioned that cond is a more traditional version of if. A cond statement in newLISP has the following structure:
(cond
(test action1 action2 etc)
(test action1 action2 etc)
(test action1 action2 etc)
; ...
)
where each list consists of a test followed by one or more expressions or actions that are evaluated if the test returns true. newLISP does the first test, does the actions if the test is true, then ignores the remaining test/action loops. Often the test is a list or list expression, but it can also be a symbol or a value.
A typical example looks like this:
(cond
((< x 0) (define a "impossible") )
((< x 10) (define a "small") )
((< x 20) (define a "medium") )
((>= x 20) (define a "large") )
)
which is essentially the same as the if version, except that each pair of test-actions is enclosed in parentheses. Here's the if version for comparison:
(if
(< x 0) (define a "impossible")
(< x 10) (define a "small")
(< x 20) (define a "medium")
(>= x 20) (define a "large")
)
For simpler functions, it might be easier to work with if. But cond can be more readable when writing longer programs. If you want the action for a particular test to evaluate more than one expression, cond's extra set of parentheses can give you shorter code:
(cond
((< x 0) (define a -1) ; if would need a begin here
(println a) ; but cond doesn't
(define b -1))
((< x 10) (define a 5))
; ...
Variables local to a control structure
[edit | edit source]The control functions dolist, dotimes, and for involve the definition of temporary local symbols, which survive for the duration of the expression, then disappear.
Similarly, with the let and letn functions, you can define variables that exist only inside a list. They aren't valid outside the list, and they lose their value once the list has finished being evaluated.
The first item in a let list is a sublist containing variables (which don't have to be quoted) and expressions to 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:
(let
(x (* 2 2)
y (* 3 3)
z (* 4 4))
; end of 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 - although the entire expression returns the value 16, the value returned by the final println statement.
The structure of a let list is easily remembered if you imagine it on a single line:
(let ((x 1) (y 2)) (+ x y))
If you want to refer to a local variable elsewhere in the first, 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, you can refer to the value of x, which we've only just defined to be 2. letn, a nested version of let, allows you to do this.
Our discussion of local variables leads to functions.
Make your own functions
[edit | edit source]The define function provides a way to store a list of expressions under a name, suitable for running later. The functions you define can be used in the same way as newLISP's built-in functions. The basic structure of a function definition is like this:
(define (func1)
(expression-1)
(expression-2)
; ...
(expression-n)
)
when you don't want to supply any information to the function, or like this, when you do:
(define (func2 v1 v2 v3)
(expression-1)
(expression-2)
; ...
(expression-n)
)
You call your newly defined function like any other function, passing values to it inside the list if your definition requires them:
(func1) ; no values expected
(func2 a b c) ; 3 values expected
I say expected, but newLISP is flexible. You can supply any number of arguments to func1, and newLISP won't complain. You can also call func2 with any number of arguments - in which case a, b, and c are set to nil at the start if there aren't enough arguments to define them.
When the function runs, each expression in the body is evaluated in sequence. The value of the last expression to be evaluated is returned as the function's value. For example, this function returns either true or nil, depending on the value of n:
(define (is-3? n)
(= n 3))
> (println (is-3? 2)) nil > (println (is-3? 3)) true
Sometimes you'll want to explicitly specify the value to return, by adding an expression at the end that evaluates to the right value:
(define (answerphone)
(pick-up-phone)
(say-message)
(set 'message (record-message))
(put-down-phone)
message)
The message at the end evaluates to the message received and returned by (record-message) and the (answerphone) function returns this value. Without this, the function would return the value returned by (put-down-phone), which might be just a true or false value.
To make a function return more than one value, you can return a list.
Symbols that are defined in the function's argument list are local to the function, even if they exist outside the function beforehand:
(set 'v1 999)
(define (test v1 v2)
(println "v1 is " v1)
(println "v2 is " v2)
(println "end of function"))
(test 1 2)
v1 is 1 v2 is 2 end of function > v1 999
If a symbol is defined inside a function body like this:
(define (test v1)
(set 'x v1)
(println x))
(test 1)
1 > x 1
it's accessible from outside the function as well. That's why you'll want to define local variables! See Local variables.
newLISP is smart enough not to worry if you supply more than the required information:
(define (test)
(println "hi there"))
(test 1 2 3 4) ; 1 2 3 4 are ignored
hi there
but it won't fill in gaps for you:
(define (test n)
(println n))
>(test) ; no n supplied, so print nil nil > (test 1) 1 > (test 1 2 3) ; 2 and 3 ignored< 1
Local variables
[edit | edit source]Sometimes you want functions that change the values of symbols elsewhere in your code, and sometimes you want functions that don't - or can't. The following function, when run, changes the value of the x symbol, which may or may not be defined elsewhere in your code:
(define (changes-symbol)
(set 'x 15)
(println "x is " x))
(set 'x 10)
;-> x is 10
(changes-symbol)
x is 15
If you don't want this to happen, use let or letn to define a local x, which doesn't affect the x symbol outside the function:
(define (does-not-change-x)
(let (x 15)
(println "my x is " x)))
(set 'x 10)
> (does-not-change-x) my x is 15 > x 10
x is still 10 outside the function. The x inside the function is not the same as the x outside. When you use set to change the value of the local x inside the function, it doesn't change any x outside:
(define (does-not-change-x)
(let (x 15) ; this x is inside the 'let' form
(set 'x 20)))
>(set 'x 10) ; this x is outside the function 10 > x 10 > (does-not-change-x) > x 10
instead of let and letn you can use the local function. This is like let and letn, but you don't have to supply any values for the local variables when you first mention them. They're just nil until you set them:
(define (test)
(local (a b c)
(println a " " b " " c)
(set 'a 1 'b 2 'c 3)
(println a " " b " " c)))
(test)
nil nil nil 1 2 3
There are other ways of declaring local variables. You might find the following technique easier to write when you're defining your own functions. Watch the comma:
(define (my-function x y , a b c)
; x y a b and c are local variables inside this function
; and 'shadow' any value they might have had before
; entering the functions
The comma is a clever trick: it's an ordinary symbol name like c or x:
(set ', "this is a string")
(println ,)
"this is a string"
- it's just less likely that you'd use it as a symbol name, and so it's useful as a visual separator in argument lists.
Default values
[edit | edit source]In a function definition, the local variables that you define in the function's argument list can have default values assigned to them, which will be used if you don't specify values when you call the function. For example, this is a function with three named arguments, a, b, and c:
(define (foo (a 1) b (c 2))
(println a " " b " " c))
The symbols a and c will take the values 1 and 2 if you don't supply values in the function call, but b will be nil unless you supply a value for it.
> (foo) ; there are defaults for a and c but not b
1 nil 2
(foo 2) ; no values for b or c; c has default
2 nil 2
> (foo 2 3) ; b has a value, c uses default
2 3 2
> (foo 3 2 1) ; default values not needed
3 2 1
>
Arguments: args
[edit | edit source]You can see that newLISP is very flexible with its approach to arguments to functions. You can write definitions that accept any number of arguments, giving you (or the caller of your functions) maximum flexibility.
The args function returns any unused arguments that were passed to a function:
(define (test v1)
(println "the arguments were " v1 " and " (args)))
(test)
the arguments were nil and ()
(test 1)
the arguments were 1 and ()
(test 1 2 3)
the arguments were 1 and (2 3)
(test 1 2 3 4 5)
the arguments were 1 and (2 3 4 5)
Notice that v1 contains the first argument passed to the function, but that any remaining unused arguments are in the list returned by (args).
With args you can write functions that accept different types of input. Notice how the following function can be called without arguments, with a string argument, with numbers, or with a list:
(define (flexible)
(println " arguments are " (args))
(dolist (a (args))
(println " -> argument " $idx " is " a)))
(flexible)
arguments are ()
(flexible "OK")
arguments are ("OK")
-> argument 0 is OK
(flexible 1 2 3)
arguments are (1 2 3)
-> argument 0 is 1
-> argument 1 is 2
-> argument 2 is 3
(flexible '(flexible 1 2 "buckle my shoe"))
arguments are ((flexible 1 2 "buckle my shoe"))
-> argument 0 is (flexible 1 2 "buckle my shoe")
args allows you to write functions that accept any number of arguments. For example, newLISP is perfectly happy for you to pass a million arguments to a suitably defined function. I tried it:
(define (sum)
(apply + (args)))
(sum 0 1 2 3 4 5 6 7 8
; ...
999997 999998 999999 1000000)
; all the numbers were there but they've been omitted here
; for obvious reasons...
;-> 500000500000
In practice, newLISP was happy with this but my text editor wasn't.
The doargs function can be used instead of dolist to work through the arguments returned by args. You could have written the flexible function as:
(define (flexible)
(println " arguments are " (args))
(doargs (a) ; instead of dolist
(println " -> argument " $idx " is " a)))
newLISP has yet more ways to control the flow of code execution. As well as catch and throw, which allow you to handle and trap errors and exceptions, there's silent, which operates like a quiet version of begin.
If you want more, you can write your own language keywords, using newLISP macros, which can be used in the same way that you use the built-in functions. See Macros.
Scope
[edit | edit source]Consider this function:
(define (show)
(println "x is " x))
Notice that this function refers to some unspecified symbol x. This may or may not exist when the function is defined or called, and it may or may not have a value. When this function is evaluated, newLISP looks for the nearest symbol called x, and finds its value:
(define (show)
(println "x is " x))
(show)
x is nil
(set 'x "a string")
(show)
x is a string
(for (x 1 5)
(dolist (x '(sin cos tan))
(show))
(show))
x is sin x is cos x is tan x is 1 x is sin x is cos x is tan x is 2 x is sin x is cos x is tan x is 3 x is sin x is cos x is tan x is 4 x is sin x is cos x is tan x is 5
(show)
x is a string
(define (func x)
(show))
(func 3)
x is 3
(func "hi there")
x is hi there
(show)
x is a string
You can see how newLISP always gives you the value of the current x by dynamically keeping track of which x is active, even though there might be other xs lurking in the background. As soon as the for loop starts, the loop variable x takes over as the current x, but then that x is immediately superseded by the list iteration variable x which takes the value of a few trigonometric functions. In between each set of trig functions, the loop variable version of x pops back again briefly. And after all that iteration, the string value is available again.
In the func function, there's another x which is local to the function. When show is called, it will print this local symbol. A final call to show returns the very first value that x had.
Although newLISP won't get confused with all those different x's, you might! So it's a good idea to use longer and more explanatory symbol names, and use local rather than global variables. If you do, there's less chance of you making mistakes or of misreading your code at a later date. In general it's not a good idea to refer to an undefined symbol in a function unless you know exactly where it came from and how its value is determined.
This dynamic process of keeping track of the current version of a symbol is called dynamic scoping. There'll be more about this topic when you look at contexts (Contexts). These offer an alternative way to organize similarly-named symbols - lexical scoping.