In the previous instalment, we introduced this series of articles whose purpose is to explore, using a very simple arbitrary example, the idea of using Common Lisp to interactively discover and develop a program requirements definition which would then be implemented in Ada 2012. This is a type of prototype model, except that the prototype is not so much the implementation of a specification as it is the specification itself.
We already talked about the requirements of the example "omer count" program in that first article, but in reality, many of those detailed requirements or specifications were developed during the course of interactively creating the script itself. The purpose of the present article, then, is to retrospectively give some idea of the sort of process which was followed in developing the simple program or script.
We will not review here the purpose or requirements of the example program. Those interested are encouraged to quickly review the previous article first before continuing.
It was known at the start that the program would never be anything more than a simple script, so it was written that way from the beginning. For example:
- Everything is in one small text file
- There is no system definition detailing module dependencies. (The one external library dependency is noted as a comment at the top of the file.)
- A few general purpose, self-written library functions are copy and pasted in preference to using the whole library, for no other reason than that happened to be inconvenient at the time.
One thing that I did do which wasn't necessary, was to create a dedicated package to accommodate the script without polluting the CL-USER package.
Here are the top few lines of the program, which were written immediately to get started:
The first line is a comment noting the dependency on the DATE-CALC library, which provides a DELTA-DAYS function for calculating the number of days between one date and another.
The DEFPACKAGE form defines a package called DKT.OMER, and specifying a shorter "nickname" of OMER. The COMMON-LISP package symbols are used or "imported", and the symbols OMER and OMER-FOR are exported. (These exports were actually added later.)
The IN-PACKAGE form sets the current package to DKT.OMER, so that all unqualified symbols from then onwards refer to symbols in that package. (When things like functions and variables are given names in Lisp, symbols are used for the purpose.)
The DEFPARAMETER form creates a dynamic special variable, or parameter (hence the name) to the program, called *PESACH* which will store the required base date from which the count is to begin.
Starting Interactive Development with a Lisp Image
At present, all we have is a few lines of text but no live code. Now, to most programmers, that is a confusing statement. This is because Lisp development typically occurs within a live, running Lisp environment, and not in a batch-like edit, compile, run cycle, as happens typically in most other program development environments.
Since my Lisp IDE consists of the combination of Emacs and SLIME (Superior Lisp Interaction Mode for Emacs), all I do to start interactive development is type in Emacs "ALT-X slime <enter>" and within a second or two I have an interactive REPL (Read Eval Print Loop) prompt within my editor.
After that, I want to load the DATE-CALC library, downloading and compiling it, if necessary, by typing the form: (ql:quickload "date-calc").
Then I compile my omer.lisp file containing the above few lines of code by simply pressing CTRL C K.
And lastly, to switch the REPL into the OMER package, I just type CTRL C ALT P OMER <enter> or alternatively type the following equivalent form at the REPL prompt: (in-package #:dkt.omer).
It's time to get started with interactively building up the required functionality. The most basic requirement is that we need a count of days, so we can start by entering something like this at the REPL:
Output looks good except that we never want a day count of zero. Now, since we are counting up to day fifty, it would be nice to also be told how many days are left, so let's add that:
Day fifty is our target (Shavuot), so all is looking as expected so far as counting is concerned. However, we want the day count to be an ordinal number in words (fourteenth, twenty-second, and so on), we want the number of days remaining to likewise be written as a cardinal number in words (twenty, thirty-four, and so on), and we want singular and plurals in the output to be correct (we don't want to read, "there are one days to go!") This can be done as follows by adjusting the call to FORMAT:
Note that FORMAT is a very powerful sub-language in Common Lisp, the details of which can be found online here.
Now that we are thinking about how far left to go until Shavuot in our counting, we realise that it would be nice to note when we reach half way! So let's go ahead and add that little note:
Apart from the day count, we also want to know which week it is since Pesach, and which day of that week it is. Because Pesach can fall on any day of the week, the "day of that week" just mentioned is not equivalent to the normal days of the week. After a little mucking around with modulus arithmetic, I came up with the following code:
Finally, we also want to keep track of how many Sabbaths (Saturdays) we have counted along the way. To do this, it would be nice to have a function which would return a count of Sabbaths from the day after Pesach up to and including a given date. Here is one that works, along with a supporting function to increment any given date:
Note that it would certainly be possible to come up with some clever formula which calculates the same answer; however, this method is easier to verify correct, and any lack of efficiency in calculation is purely notional given the short time periods involved in the calculations. In fact, we can easly calculate the number of Sabbaths over a time span of a thousand years within a couple of seconds, which is more than good enough for our purposes!
Now that we have a Sabbath count function, it is very easy to add the Sabbath count to our original code, as shown below:
We're getting pretty close now, but we have accumulated a few lines of code at the REPL, so it's probably time to formalise this into an actual callable function. Since the function outputs the required omer count for a given date, we will just call the function OMER-FOR, as follows:
All we have done here is move DATE so that it becomes the function parameter, and we have added a heading line to the output. So now we can write:
We're nearly there. We have taken care of the normal cases of counting; that is, where the day count is greater than zero and less than fifty. We need to do something different when the day count is:
- equal to zero,
- equal to fifty,
- greater than fifty,
- less than zero.
Since the code so far represents the normal case, we will encapsulate it in its own local function and call it NORMAL-COUNT, as follows:
FLET means Function LET. In this example, it is binding the five forms of our code so far (FORMAT, WHEN, FORMAT, FORMAT, and LET) to the name NORMAL-COUNT, so that we can call (NORMAL-COUNT) when we want to evaluate or execute that code.
In the place of the comment, "Normal and other cases go here," we need to put a conditional expression to take care of all five cases (the four noted above, plus the normal case). In Lisp we use a CONDitional expression for that, as follows:
In the first case, where (< 0 day-count 50), i.e., when the day count is between zero and fifty but not equal to zero or fifty, we want to evaluate the code for NORMAL-COUNT.
In the second case, where (= 0 day-count), i.e., when the day count is equal to zero, we want to print out a single line message noting that the day is Pesach.
In the third case, where (= 50 day-count), i.e., when the day count is equal to fifty, we want to print a message saying that the day is... and then print a banner that reads "SHAVUOT".
In the fourth case, where (> day-count 50), i.e., when the day count is greater than fifty, we want to print a single line message saying that Shavuot has passed.
In the fifth and final case, which is when the day count is negative, we want to print a single line message saying that it doesn't make any sense, since we don't start the count until the day after Pesach.
Inserting this conditional code, we now have:
So, let's try it out:
All looks good, but we can note two things. First, when using the REPL for this, as is the intention, it is slightly annoying to see the NIL return value printed after each call. Second, we have conveniently skipped showing the code to generate the banner. The fix for the NIL issue is simply to return something besides NIL at the end of the OMER-FOR function, for instance, the symbol OMER-COUNT. So, we now have:
And when we run it, we get OMER-COUNT as the evaluation result instead of NIL:
The code to generate the banner is as follows. It simply consists of one function per letter, and one function to print the required word. Each letter function defines a string vector of length six, since each letter consists of six lines of print, and prints out one of those six lines depending on the value provided for the LINE parameter. Since these "functions" simply print output, and the return value is ignored, they are really "procedures" called only for their so-called "side effects".
There is one final finishing touch. During the period of counting, the computer is quite capable of knowing what today's date is, so we don't need to tell it all the time; hence, we just want a function, call it OMER, which supplies today's date to OMER-FOR:
We have now accomplished a couple of things. First, we have finished defining what the requirements are for this application, and these requirements are embodied in the "executable pseudo-code", except that the pseudo code is real Lisp code (which is why it is executable).
The second thing we have at least attempted to accomplish is to hint at the dynamic nature of interactive and incremental development in Lisp, which is quite unlike most other languages, including other languages which may boast a "REPL". Having a REPL is far from the only requirement for a fully immersive, interactive development environment.
We are now ready to take our design specification and use it to develop a type-safe, binary executable application in the Ada 2012 language. For this exercise, we are going to forget that the whole point of this particular program was to develop an easily-modifiable script which can be conveniently executed from within the REPL. (Note that the REPL is not just for development! It is also for executing finished, "production" code.)
Until then, happy programming!
Addendum: Full Source Code of the Design Specification
The only code omitted consists of a couple of functions from the DATE-CALC library. The source for these can be obtained if it is required to code equivalent functionality ourselves in the target Ada language.