Page 1 of 1
Inconsistent syntax of "case"
Posted: Tue Jan 13, 2009 3:12 am
by seetho
I'm going through Cormullion's Introduction to newLISP. Everything was going down really smoothly. The syntax was very consistent and simple (in a good way) and everything governed by the 3 simple rules, until I came to the case statement example. Syntax consistency was what drew me to newLISP in the first place (away from CL and Scheme).
I found that the "exp-switch" part is not evaluated as default. This really (to a newbie like me) sticks out like a very tiny splinter on an otherwise very smooth piece - if you don't hold it just right it'll prick you!
Shouldn't the exp-switch be evaluated like everything else to keep consistency? You can always stop the evaluation using quotes. I think there's a macro that does evaluation, but shouldn't that be the rule instead of the exception?
I may be missing a bigger picture here, so I'd appreaciate some comments from any newLISP gurus here.
Thanks.
Posted: Tue Jan 13, 2009 3:42 am
by Lutz
There are several other built-in functions in newLISP where arguments are not evaluated. In the case of the 'case' function, newLISP does what users from other LISPs would expect. Assuming unevaluated constants make for a faster, efficient and more readable (explicit) case-statement. Use 'cond' or 'if' to conditionally branch on evaluated expressions.
Posted: Tue Jan 13, 2009 9:00 am
by seetho
Hi Lutz,
Now it makes more sense that you are calling it "constants". It naturally comes to mind that constants should not be evaluated. Maybe it should be called "const-switch" instead of "exp-switch" in the manual. Just my humble opinion.
Thanks for clearing it up.
Posted: Tue Jan 13, 2009 4:39 pm
by cormullion
Hi seetho - thanks for reading my Intro! :)
There's a fine line between simplifying and over-simplifying - in my attempts to explain things to myself and others I hope I haven't conveyed the impression that there are 'only' three simple rules with no exceptions...
But I think that newLISP is fairly consistent, both with itself, and with other languages. But not 100% consistent in both directions.... That would be a bit extreme. ;)
The subtleties of evaluation is the thread that runs from the very first pages of an introduction right through to the advanced expressions that make my head hurt (and that you won't find in anything I write..!)..
Posted: Tue Jan 13, 2009 6:14 pm
by seetho
Hi cormullion,
Your tutorial is a great help in my learning newLISP. I count programming as one of my hobbies since my first VIC-20 - ok some of you know my age group now ;)
I think that whatever number of rules that govern a programming language, be it 3 or 30, the most important thing is consistency. (Although I really wish it to be just 3 for newLISP - and Why Not!?) Simple does not equate to useless. IMHO the most useful and enduring programming languages are, at the core, very simple.
Anyway I feel that newLISP has great potential. I look forward to getting enough stuffed into my head to start coding some useful applications soon.
Posted: Tue Jan 13, 2009 6:45 pm
by cormullion
Cool, thanks!
The 3 simple rules thing is of course my idea. No such thing, really - Lisp syntax has to be more complex than that. Take a look at
http://www.newlisp.org/ExpressionEvaluation.html, for example... :)
Even something as simple as an 'if' expression could be seen as an exception - for obvious reasons. Is (exit) evaluated first?
Posted: Tue Jan 13, 2009 7:20 pm
by DrDave
cormullion wrote:Cool, thanks!
Even something as simple as an 'if' expression could be seen as an exception - for obvious reasons. Is (exit) evaluated first?
But doesn't the question of (exit) evaluation fall under lazy evaluation, hence making the evaluation of 'if' expressions more of a nuance than an actual exception to argument evaluation?
Posted: Tue Jan 13, 2009 9:28 pm
by Kazimir Majorinc
IF is traditional example of what cannot be done as function in Common Lisp or Scheme. But, it can be done in Newlisp, because Newlisp - beside static scope in contexts - has very powerful dynamic scope support.
Code: Select all
(set 'IF (lambda(condition)
(let ((z (find (eval condition) '(doesntmatter nil))))
(eval (nth (dec (inc z)) $args)))))
(let ((x 2)(y 3))
(IF '(< x y)
'(println x " is less than " y)
'(println x " is not less than " y))
(IF '(> x y)
'(println x " is greater than " y)
'(println x " is not greater than " y)))
; 2 is less than 3
; 2 is not greater than 3
;
Inc dec thingy looks like trick, but it is not really important, the point is that IF understand its arguments, they are not only empty syntax like in mainstream Lisps. So, unlike CL and Scheme, in Newlisp, it is matter of convenience whether some argument is evaluated or not. There was such discussion about inc, i.e
(inc x) vs
(inc 'x). It is matter of convenience for users, and
Lutz treats bulit in primitives on similar way.
In long terms, maybe some syntax colouring could be used in manual to indicate expressions treated on "macro-like" way. For example
(if condition then-part else-part)
But it is not really that simple.
Posted: Wed Jan 14, 2009 12:12 pm
by seetho
At first look, the IF statement is really puzzling if you stubbornly insist that everything in a list is evaluated (sequentially), but then if you look again carefully as DrDave mentions it still is consistent.
If you think in terms of everything being processed in parallel then it may easier to imagine. Take for example that this list/expression if received by a super multi-processor computing environment. The statement
(IF (= 5 (+ 2 2)) "ok" (exit))
get passed to processor#1 to handle. Processor#1 then decides that it can pass on
(= 5 (+ 2 2)) to processor#2, "ok" to processor#3 and (exit) to processor#4.
Everything is now evaluated in parallel and processor#1 knows that the IF function indicated that it needs to decide which result to use (either from processor#3 or #4) will depend on what's return by processor#2. Of course processor#2 further divides up the work to more processors, but eventually it figures out that it should return NIL.
Upon receiving the NIL result from processor#2, processor#1 decides to take the result of processor#3. The result of processor#4 is discarded. Even if processor#4 went ahead and evaluated the (exit) expression it is not necessary that it is executed, it merely returns and EXIT signal back to processor#1. Whether it is used or not and how this signal is ultimately handled is entirely up to processor#1.
So conceptually everything is evaluated.
Did that make sense?
Posted: Wed Jan 14, 2009 2:23 pm
by DrDave
seetho wrote:So conceptually everything is evaluated.
Did that make sense?
It made sense to me. In a nutshell, if I understand correctly, you are saying in your multi-processor example that all the arguments indeed are evaluated, but it is up to the 'If function to make a determination of how to proceed after the results of the evaluation of the arguments occurs, However, I think in newLISP that if the first argument evaluates to true, then the second argument is never processed (lazy evaluation), thereby saving the computation time, etc., that would be required for that evaluation.
Posted: Wed Jan 14, 2009 2:39 pm
by seetho
DrDave wrote:However, I think in newLISP that if the first argument evaluates to true, then the second argument is never processed (lazy evaluation), thereby saving the computation time, etc., that would be required for that evaluation.
Let's just say the 2nd argument "did not need to be" evaluated instead of "never" because it could have been evaluated if we wanted to. Just that the results will not be used. Of course in practice, resource efficiency is important so "lazy evaluation" is adopted to cut down on unneccessary work. I can imagine that in a multi-threaded enviroment optimised for speed, the 2nd argument would've been evaluated in any case.
Posted: Wed Jan 14, 2009 2:49 pm
by Kazimir Majorinc
seetho wrote:(IF (= 5 (+ 2 2)) "ok" (exit))
get passed to processor#1 to handle. Processor#1 then decides that it can pass on
(= 5 (+ 2 2)) to processor#2, "ok" to processor#3 and (exit) to processor#4.
Everything is now evaluated in parallel and processor#1 knows that the IF function indicated that it needs to decide which result to use (either from processor#3 or #4) will depend on what's return by processor#2.
...
So conceptually everything is evaluated.
Did that make sense?
Actually, there is a problem, because in
if statetement, it is not only result of the evaluation of
then-branch and
else-branch that matters, but
side effects matter as well. So, in your mental model, it is not enough that processor #1 decides which result it will use (#3 or #4), it is important that evaluation of the "
wrong one" never actually happened.
Unfortunately, when you see Newlisp expression
(f expr1 expr2 ... ) you cannot know whether
expr1 expr2 ... are evaluated if you do not know the value of f. You can define your own IF, CASE etc. with semantics consistent on the way you want (see my IF above) if you really want that. Not that I advice it, especially not at the beginning of using Newisp, but it is possible.
Going back to your original question, in
(case exp-switch (exp-1 body-1) [(exp-2 body-2) ... ])
exp-switch is evaluated, thats why it is possible to write
Code: Select all
(set 'n 3)
(println (case n
(1 "one")
(2 "two")
(3 "three")
(4 "four")))
but other parts are not evaluated as default, only by demand. That's why you cannot write something like
Code: Select all
(set 'n 3)
(set 'm 3)
(println (case n
(1 "one")
(2 "two")
(m "three"); n=m not recognized
(4 "four")))
Posted: Wed Jan 14, 2009 4:07 pm
by seetho
Kazimir Majorinc wrote:Actually, there is a problem, because in if statetement, it is not only result of the evaluation of then-branch and else-branch that matters, but side effects matter as well. So, in your mental model, it is not enough that processor #1 decides which result it will use (#3 or #4), it is important that evaluation of the "wrong one" never actually happened.
So in the example of an
IF statement you'd have to wait for the
PREDICATE clause to return before deciding on executing the
THEN clause or the
ELSE clause. If I understand you correctly, this is because the
PREDICATE clause may have some side effects on either the
THEN or
ELSE clauses, making it impossible to evaluate them in parallel. Still either one will get evaluated eventually.
(case exp-switch (exp-1 body-1) [(exp-2 body-2) ... ])
My original gripe was that exp-1 implied that it was evaluated. It would've been better to call it const-1 so that you get:
Code: Select all
(case exp-switch (const-1 body-1) [(const-2 body-2) ... ])
You see "const" then you know that you cannot put an expression there. I know it seems a petty matter to those already familiar with the concept, but to newbies like me emerging from decades of languishing in the "dark side" of imperative languages, it does make things easier to grasp.
Posted: Wed Jan 14, 2009 6:08 pm
by DrDave
seetho wrote:
Let's just say the 2nd argument "did not need to be" evaluated instead of "never" because it could have been evaluated if we wanted to. Just that the results will not be used.
I don't think that is correct for newLISP. My understanding is that in the case where the first argument is true, it is impossible to move the instruction pointer to run the code of the second argument. Hence, we cannot evaluate the second argument, even if we want to evaluate it. But if you know how to evaluate both arguments when the first argument is true, I'd be happy to see the code. That does not mean that you add some code to the first argument so that it somehow grabs the second argument and does the evaluation, or some other similar odd convolution.
Posted: Wed Jan 14, 2009 6:26 pm
by Kazimir Majorinc
It is because ELSE or THEN clause can have side effects. Look at this:
Code: Select all
(if (< x y)
(begin (inc lesser) x)
(begin (inc greater) y))
If both clauses are evaluated, then both
lesser and
greater are incremented by one, while one probably wants only one of these. Lisp is not "
pure functional" language. One can write code without side effects, but they're allowed, and
if has to care about that.
Code: Select all
(case exp-switch (const-1 body-1) [(const-2 body-2) ... ])
Yes, it has sense. It is simple idea that can make things cleaner.
No problems about question, ask as many times you need. These issues are not that obvious, really, there are some subtleties one cannot see on the first sight. But, it is easier to understand Newlisp than CL and Scheme.
Posted: Wed Jan 14, 2009 7:04 pm
by DrDave
seetho wrote:My original gripe was that exp-1 implied that it was evaluated. It would've been better to call it const-1 so that you get:
Code: Select all
(case exp-switch (const-1 body-1) [(const-2 body-2) ... ])
You see "const" then you know that you cannot put an expression there. I know it seems a petty matter to those already familiar with the concept, but to newbies like me emerging from decades of languishing in the "dark side" of imperative languages, it does make things easier to grasp.
I think you make a good point here for a change to the wording in the manual. In fact, along with some additional clarification, because even if you try to force evaluatioin with eval, it fails. Also, if you set a symbol as a constant and try to use that constant, thinking that a constant symbol would surely work, it also fails. So as far as I can tell, you must write the actual number or string to match. I think I might want to be able to evaluate an expresion on the fly whose result is what case is being matched against. For example, perhaps I would like my case statements to be something like
Code: Select all
("item-1" "found 1")
("item-2" "found 2")
And I have a hundred of these.
I would like to code my case statement something like this
Code: Select all
; do some code...
(setf x 2)
; do some code...
(setf mystring "item-"
(case str-to-match
((string mystring x) (string "found " x) ))
But of course it won't give the desired output, because the way case is currently implemented, it won't evaluate the first part.
Posted: Wed Jan 14, 2009 7:18 pm
by Kazimir Majorinc
DrDave wrote:I would like to code my
case statement something like this
Code: Select all
; do some code...
(setf x 2)
; do some code...
(setf mystring "item-"
(case str-to-match
((string mystring x) (string "found " x) ))
I think
letex is perfect for that. Something like (but I didn't tested it)
Code: Select all
(letex ((y (string mystring x)))
(case str-to-match
(y (string "found" x))))
Lutz did few such things really hitting the target.
Posted: Wed Jan 14, 2009 10:35 pm
by DrDave
Kazimir Majorinc wrote:
I think
letex is perfect for that. Something like (but I didn't tested it)
Code: Select all
(letex ((y (string mystring x)))
(case str-to-match
(y (string "found" x))))
Lutz did few such things really hitting the target.
Now that is very interesting! Why does letex create a variable that seems to get evaluated?
This indeed works
Code: Select all
(for (x 1 5)
(begin
(setq mystring "item-" )
(setq str-to-match (string "item-" x))
(letex ((y (string mystring x)))
(case str-to-match
(y (println (string "found " x)))
))))
-->
found 1
found 2
found 3
found 4
found 5
Posted: Thu Jan 15, 2009 12:20 am
by Kazimir Majorinc
Why does letex create a variable that seems to get evaluated?
Because
letex creates new expression such that
x is replaced with its value, and evaluates that new expression. These two are equivalents (
letex =
let &
expand.)
Code: Select all
(println (letex ((x 3))
(+ x 3)))
(println (let((x 3))
(eval (expand '(+ x 3)
'x))))
Posted: Thu Jan 15, 2009 10:52 am
by DrDave
So just to clarify, we can use letex to set a variable to use in the case selections, but we can't put an expression directly in place of the variable.
Code: Select all
(set 'n 3 'x 24 'y 8)
(letex ((m (/ x y)))
(println (case n
(1 "one")
(2 "two")
(m "three")
(4 "four"))))
--> "three"
Posted: Thu Jan 15, 2009 11:02 am
by DrDave
Kazimir Majorinc wrote:Why does letex create a variable that seems to get evaluated?
Because
letex creates new expression such that
x is replaced with its value, and evaluates that new expression. These two are equivalents (
letex =
let &
expand.)
Code: Select all
(println (letex ((x 3))
(+ x 3)))
(println (let((x 3))
(eval (expand '(+ x 3)
'x))))
Ah, now I see. It is similar to passing arguments to a function by value rather than by reference. When we use expand, the VALUE bound to the symbol replaces the symbol in the expression, and then this new expression containing the values gets evaluated.
Posted: Thu Jan 15, 2009 1:47 pm
by seetho
DrDave wrote:
Now that is very interesting! Why does letex create a variable that seems to get evaluated?
Functions are left associative so, the
letex expression gets evaluated first, so all y gets substituted and evaluated to its final value before
case get evaluated, and by then it sees the final value there as a constant.