Hello, all--
I have a question that is not Chicken-specific, or even scheme-specific,
but since it relates to a project I'm developing in Chicken Scheme, I hope
you will humor me.
I have been working on a framework for interactive command-line programs. I
have an initial version that works for my needs; I was thinking of
releasing my work as an egg, but I now believe that the library is rather
poorly designed. So I'm going to use it in its present form, but if I am
going to release it to the public, I need to fundamentally redesign it. The
basic problem is that there is a lot of special-case code to deal with some
complex data types that are important for me, and possibly to others. Thus
I think the code will be a nightmare to maintain; furthermore, it occurs to
me that what this library provides is (or can be seen as) a form of finite
state machine. Yet that model is not clearly expressed in the current
code--I'd say there is no coherent architectural principle at all.
To be more specific, here's a very simple example of what you can do with
the current version:
(use text-menu)
(interact
(steps
(first: (make-step 'first-name))
(second: (make-step 'last-name))
(third: (make-step 'phone required: #f))
(fourth: (make-step 'email required: #f)))
looping: #t)
(get-all-data)
This is working code. If you run it, it presents prompts like "first-name:
", and record the input ... and since 'looping' is true, when you finish
one the series once, you have a choice to start over or quit ... and
afterwards a call to (get-all-data) retrieves all the data that was
collected. Now, that 'make-step' function can be customized in various ways
via keyword arguments ... you can specify a customized prompt, a default
value, validators, actions to perform based on the input, and a few other
things. So you can imagine already that there is a lot of internal
complexity. There are also several specialized 'make-step' functions for
handling specialized data types, and a couple of those are quite complex
indeed.
The general procedure for handling each prompt-and-input cycle looks like
this (in pseudocode):
show prompt
input = get-input
valid = validate input
if not valid -> handle error
perform action on input
record input
choose next step
But: there are these important exceptions I mentioned, which necessitate a
process like this:
show prompt
raw input = get-input
-> input = preprocess input
valid = validate input
if not valid -> handle error
perform action on input
record input
choose next step
Now, I see two big trouble spots here:
1) the input and output of the preprocessing step necessitates
modifications to some of the other steps,
because although much of the data is just strings, there are other
types that are more usable as lists
or more complex structured types; and
2) the invalid input handling is special case code; I imagine in a
properly architected state machine it
would just be another prompt-and-input cycle.
I'll go into a bit more detail about the preprocessing problem below. But
first, let me say it seems to me there are 3 basic approaches to this
situation:
a) provide specialized procedures for specialized data types; not too
different from the current design;
b) allow preprocessing functions to return arbitrary data types, and
trust that application developers are
smart enough to provide appropriate functions to work together with
those preprocessors; or
c) standardize all data -- i.e. everything is handled internally as a
string, or as a list, etc.
Here are the two main examples of specialized data that are messing me up:
DATES
---------
I want to be able to handle dates in a flexible and user-friendly manner.
Specifically:
* The user should be able to enter dates in any of several common formats:
"2012-09-03," "9/3/2012", "Sept. 3, 2012," etc. (actually only ISO format
and US-style MM/dd/YY[YY] styles are currently supported.
* It is possible to enter abbreviated dates, e.g. MM/dd/YY, or MM-dd, and
have the date default to the current year, month, etc.
* You can specify a range of allowed years (default: 1-3000 AD)
* The preprocessor extracts the year, month, and day using irregex
submatches, and returns a list of integers: '(Y M D).
* The validator checks the '(Y M D) using integer arithmetic, so that, for
example, years outside the allowed range are rejected, dates > 31, or 30,
or 29, or 28 are rejected, depending on the month.
ENUMS
---------
These are basically predefined (optionally extensible) lists of items for
the user to choose from. So a [simplified] enum might look like:
'((copy . "Copy a file") (move . "Move a file") (delete . "Delete a file"))
This gives rise to a display something like this:
1) Copy a file
2) Move a file
3) Delete a file
And if the user enters "2", the preprocessor extracts from the enum the
symbol for Item #2, which is 'move ... we don't want to use "2" internally,
because it might not always have the same thing ... and then the validator
really has nothing to do ... or the validator should be combined with the
preprocessor? Either way, the data is not a string like most other data,
which complicates matters. I suppose the enum *could* simply be list of
strings, but I feel it is preferably to use the symbol internally, so we
can do something like:
(case file-op
((copy) (copy-action))
((move) (move-action))
((delete) (delete-action))
(else #OOPS!))
....
So, hopefully that's enough info for you to understand the design
challenge. As I said, I'm not sure if I will be releasing some form of this
library or not, but even if I don't, I'd like to have a better
understanding of how to design things like this. Thanks in advance for your
thoughts!
--
Matt Gushee
_______________________________________________
Chicken-users mailing list
[email protected]
https://lists.nongnu.org/mailman/listinfo/chicken-users