Tutorial 3 : AutoLisp : Make it pretty
Because we will be developing and using more examples in these later tutorials I wanted to give a general note/reminder, theSwamp has a built in custom code tag we can use for code snip-its in our posts that links to our own help page. Feel free to click on a function (IF, ABS, APPLY, etc) and you will be brought to the AutoLisp help for that function.
StyleThere are a few different indentation styles you can employ in/on your AutoLisp code.
One style is as old as the Lisp language itself. Another seems to be only prevalent in the AutoLisp community. Each style has good and bad attributes and it should be up to you to decide which you like better because as you will soon discover, you will spend far more of your time READING code then you do WRITING it. This is because you spend an awful amount of your time debugging and fixing errors.
I will attempt to lay out each style and give at least one solid reason for it's use.
The older style cam best be described as "compact". Many AutoLispers complain that it's much more unreadable then the other style however, I think if you know HOW to read it you may find it faster to read then the other. Here is a pretty good representation of the older ("compact") style.
(defun SquareRoot
( x
/ squareroot
-iter good
-enough? improve average
) (defun squareroot
-iter
(guess
) guess
(squareroot-iter (improve guess))) )
(defun good
-enough?
(guess
) (< (abs (- (* guess guess
) x
)) 0.001) )
(average
(list guess
(/ x guess
))) )
(squareroot-iter 1.0) )
As you can see the closing parens are not easy to match with their respective opening paren but with many programming languages you read the code based on indention and/or syntax not necessarily just symbology (there are exceptions, of course); in other words, indentation is very important. Notice the first IF statement in the "squareroot-iter" function below. The TEST, THEN, and ELSE all line up (are indented the same). This is the key; you do not try and locate the closing paren you look to see what arguments are lined up with each other. Juxtapose this with the IF statement in the "average" function, you will notice that the first use of IF has three parts while the second use of IF has only two (the TEST and the THEN). These clues, and others like them, are what guides you throughout the program and it's parts and structure.
The other style (we can call this the "newer style" but can also be called "expanded style" or "vertical style") is much more open. This style is read by matching parens to decipher the syntax's and structure.
(defun SquareRoot
( x
/ squareroot
-iter good
-enough? improve average
) (defun squareroot
-iter
(guess
) guess
(squareroot-iter (improve guess))
)
)
(defun good
-enough?
(guess
) (<
(abs (- (* guess guess
) x
)) 0.001
)
)
(average
(list guess
(/ x guess
))) )
)
)
(squareroot-iter 1.0)
)
While reviewing code, this style requires you to locate opening/closing parens because the syntax parts are not lined up as strictly as the other (thankfully, most good text editors will do this for you automatically). With this style, essentially each new line is indented 2-4 spaces more then the previous. One positive aspect of this style is the ability to digest the code in smaller chunks at a time (thus requiring you to build a sort of mind-map of the function/code as a whole).
Most AutoLispers feel the newer style is much more friendly because of it's openness whereas they feel the older style is much more cluttered. However, as an exercise I would ask that you try to study code in both styles and decide which you prefer better (just remember how you read each style; old=look to indent and line ups new=look for matching parens) and choose your style for yourself. Please also try to remember that much of your time will be spent reading and maintaining the code you, or others, write and choose a style which you feel more comfortable with.
CommentsContrary to what you may hear, even "good code" requires comments. All code (good/bad/otherwise) requires comments, for you and others LATER. I'm not saying every line should have a comment but every block of code should have a description of what it does or expects to do. A good habit to get into is to comment not only the expected behavior of the code block but also what it returns.
There has been a movement in new programming language development to create/be more "type safe"; this is because most programmers do not keep good track of the returns and types they expect. Furthermore, some people take advantage of loose function specifications and/or operations to save typing or to look like a better programmer (or for whatever reason) at the risk of an incorrect return type [-e.g. using a BOOLEAN function to ensure successful execution of a series of commands without regarding the return from that BOOLEAN wrapper function]. I'm not saying that would be a bad method all of the time--it can be an invaluable tool at times--but that specific example lends itself to a specific type/style of programming which most AutoLisp developers do not employ [-i.e. most AutoLisp developers tend to use a large sweeping and monolithic style where as the example I just gave tends to better employed in a tight, small black-box style of development where returns are more tightly controlled].
A specific experience of mine comes to mind, I started a CAD Manager position at a new company and I spent 9 months revising old convoluted code written by a predecessor. In one instance I deleted a hundred lines of code with one simple AND statement; I honestly felt bad about it because the previous coder had this (overly) complicated routine and all these comments and specifications and I come along and recognize that all of that could be a simple AND statement. However I couldn't have done that if the previous programmer didn't comment his code/thoughts (I had a very good idea as to his intentions). The previous programmer had fallen into a trap we all have at one point or another; we tend to loose sight of the forest when staring at the trees.
Because this tutorial lacks examples (is mostly wording) I will try to use another academic bit of code--which leans more towards a metaphor then usefulness--built for rational numbers to demonstrate the minimum amount of comments I feel would be necessary. This example would be more a "library of code" type of example where you may find yourself building a series of functions, built for a specific purpose, for use throughout your code.
NOTE: To show you that the rules that I discuss in these tutorials are not hard-fast rules--they are more like guidelines--I will use abbreviations. However, given the context of the code I would like you to note that there is no confusion as to meaning of the abbreviations. The abbreviations were chosen for artistic reasons (see how nicely everything lines up; notice how the "numerator", and "denominator" labels have been shorted to 5 characters, and the label "rational" was abbreviated to three characters to match the mathematical labels).
Also, I will use the "old" style for this code to give you an opportunity/exercise to read the old style of code. Remember, to fully appreciate the "old" style, pay attention to the number of arguments for the functions and the indentation of the code. You can also reformat this code in the VLIDE, Visual Studio Code, or most text editors to explore the code using the "vertical" style as well.
(defun GreatestCommonDenominator
(a b
) ;; Find the greatest common denominator of a number
a
(GreatestCommonDenominator b
(rem a b
))))
(defun make
-rat
(n d
/ x
) ;; support procedure to ''create'' a rational number
(setq x
(GreatestCommonDenominator n d
))
;; return the numerator of a rational number.
;; return the denominator of a rational number.
;; support function to print the rational number in a pretty format.
;; rational number addition.
(make-rat (+ (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
;; rational number subtraction
(make-rat (- (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
;; rational number multiply
(make-rat (* (numer x) (numer y))
(* (denom x) (denom y))))
;; rational number divide
(make-rat (* (numer x) (denom y))
(* (denom x) (numer y))))
;; does a rational number equal another.
(= (* (numer x) (denom y))
(* (numer y) (denom x))))
Notice I didn't comment on the returns--or possible returns--of each function. Should I have? ...I, personally feel, I should. The time investment of thinking about the potential returns is defiantly worth the cost; if you think about all the possible ways a function could be misused, you can choose what is and what isn't important to address in your code. Now, that isn't to say, you need to build 100% bomb-proof code all the time but you need to make that choice more often then just writing a bit of code and expecting it to never be misused.
EDIT: Fixed spelling error in "make-rat" function.