The point, at least originally, of my SExpr mini-language is to make lists of parts so it's worth talking about those a bit.
Not surprisingly, a parts list is an ordinary Lisp list. Each item in that list is like a line item on an invoice or in a bill of materials: it's a list the first element of which is the (integer) quantity of items, the second is the general type of item and the remaining elements (if any) are additional parameters defining the item. E.g.,
36 bolt M12 130 108 bolt M12 180 144 nut M12 288 roundWasher M12
specifying 36 bolts, metric nominal diameter 12 mm, length 130 mm and so on. (Yep, that's the start of the order for my first lot of galvanized fasteners for the house frame.)
Ordinary Lisp code could generate this easily enough but the point of a domain-specific language is to express things as naturally as makes sense for the actual problem so there are some particular functions added to the language to support this.
I could have implemented these functions in Lisp but I did them early on in Python, before there was enough Lisp implementation to support this.
The two functions are part
and part_list
.
part
works like def
to define a function
which creates a parts list whereas part_list
works like
progn
to evaluate a list into a parts lists.
part
can be used to create a single (leaf or atomic)
part. For example:
let M12 'M12 let M16 'M16 part (bolt size length) part (nut size) part (washer size)
creates three functions including one called bolt
which
can be invoked with two parameters, the size and length, and returns a
parts list for one such bolt.
With the REPL set to print results as trees (with the null terminators at the ends of lists omitted) and with the above declarations made:
>> bolt M12 130 .. █━━━┳━━━━ 1 ┣━━━━ bolt ┣━━━━ M12 ┗━━━━ 130
The bolt, nut and washer parts definitions here are like def
declarations with only a function name and formal parameter list but no
function body. When a part
declaration does have a
body it acts as a part list which expands to the parts listed recursively
until leaf/atomic parts are reached.
>> part .. (bolt_set size length) .. 1 bolt size length .. 2 washer size .. 1 nut size .. █ <function operatorPart.<locals>.<lambda> at 0x7fae11041bf8> >> bolt_set M16 180 .. █━━━┳━━━━ 1 ┃ ┣━━━━ bolt ┃ ┣━━━━ M16 ┃ ┗━━━━ 180 ┣━━━┳━━━━ 2 ┃ ┣━━━━ washer ┃ ┗━━━━ M16 ┗━━━┳━━━━ 1 ┣━━━━ nut ┗━━━━ M16
The body of a part
is a part list in much the way that the
body of a def
acts as a progn
.
With a progn
the evaluation processes is to evaluate
the expressions in the list in turn then return the value of the
last one.
With a part list, though, each expression in the list can contribute
directly to the result. Evaluation of each expression in the list
proceeds as:
- If an expression is a list and its head is a ':' then its
tail is evaluated and the result is discarded so it acts like a
non-last expression in a
progn
- presumably existing for its side-effects, e.g., the definition of symbols in the local context or the printing of debug information. - Otherwise, if the expression is a list and its head evaluates to an integer then it's taken directly as a line item, the tail is evaluated and should be a parts list containing zero or more items. The quantities in the parts list are multiplied by the integer given and added to the results.
- Otherwise, the expression is evaluated and added to the results (unless it's null/the empty list, in which case it's just discarded).
Suppose we wanted one long bolt every 150 mm up some posts of various heights with shorter bolts in between each pair of long bolts:
part post_bolts height : let count (head (divmod height 150)) : print count count bolt_set M12 200 (- count 1) bolt_set M12 120
which allows (reverting to non-tree output):
>> post_bolts 450 .. 3 ((3 bolt M12 200) (6 washer M12) (3 nut M12) (2 bolt M12 120) (4 washer M12) (2 nut M12))
This is reasonable but a bit irritating in that parts of the same
type appear multiple times; there's (3 nut M12)
and,
separately, (2 nut M12)
. As I was originally using this
code the Lisp interpreter was embedded in a separate Python “bom”
(bill of materials) program which merged the common lines together.
Later I made it stand-alone, reimplementing the merge in Lisp. The body of the function is only three lines but it's sufficiently inscrutable to be worth a separate blog post, perhaps. The result is a bit more compact:
>> merge_parts (post_bolts 450) .. 3 ((3 bolt M12 200) (10 washer M12) (5 nut M12) (2 bolt M12 120))
As hinted above, a part
declaration actually creates
a function in much the same way that a def
does.
The difference is how the body, if any, is evaluated.
For a leaf part (one with no body) the actual parameters are evaluated and assigned to the formals in the function execution context in the same way as they would be for a normal function then the formal parameter list is mapped to the actual values in the correct order and the resulting one-element part list is constructed.
For part
s with a body a separate evaluation path is
followed which implements the three cases listed above.
In the third case above it's noted that null results are discarded.
This is so that parts can be included conditionally.
A direct if
expression can be included in the part
body which either does or does not add in extra parts.
Similarly, functions can be called which may or may not return
some parts.
A part_list
can be put in line to create parts using
the same rules as for a part
body in contexts where
normal Lisp evaluation would happen by default, e.g., the body
of a if
or def
.
There's quite a bit going on in the following code but the
purpose of the part_list
expressions for the then and
else expressions of the final if
should be clear,
I hope:
part frame f : assert ((fb isFrame) f) : let attrs (frameAttrs f) : let ifgh (if (in? 'greenhouse attrs)) : let postSpacing (ifgh (+ 3600 150) 3600) 2 post 464 2 post 3066 1 tieBeam (- postSpacing 166) 2 nsFloorBeam 1 nsFloorBlocking (- postSpacing 150) if (in? 'gable attrs) part_list 1 nsFloorBlocking (- 1742.5 150) 1 nsFloorBlocking (- 1742.5 150 (ifgh 150 0)) (part_list)
Actually, the empty part_list
for the else part of
the if
could be omitted as it's just null and that's
what if
s with False conditions and no else part
return anyway. But it's a bit clearer, I suppose, and allows for
any future changes where part lists are more complicated so that
an empty list is not just null.
In general I've been quite happy with how these extra operations have integrated into a Lisp-like environment and allowed parts lists to be created quite naturally. Something I might do differently, though, would be to make the distinction between leaf parts and compound parts a bit more explicit than simply whether or not they have a body. In particular, it might sometimes be handy for leaf parts to have a body: for assertions to check their parameters or for debug prints for example.