SExpr: Parts Lists

An Eccentric Anomaly: Ed Davies's Blog

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:

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:

    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 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 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 parts 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:

    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)
            1 nsFloorBlocking (- 1742.5 150)
            1 nsFloorBlocking (- 1742.5 150 (ifgh 150 0))

Actually, the empty part_list for the else part of the if could be omitted as it's just null and that's what ifs 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.