Combinable Records: Implementation

Note: combinable records are deprecated

Overview

There are 2 main parts to the combinable records implementation: the combine property and the fragment types.

The combine property is fairly straightforward. It defines a procedure which is used to combine an arbitrary number of values into one final value (for example, append to combine the contents of an arbitrary number of lists). Its implementation is very similar to to the implementation of the sanitize property. For example:

(define (sanitize-it arg)
  (unless (list? arg)
    (throw 'NOOOOOOO "That's not a list =("))
  arg))

(define-record-type <t> t make-t t? this-t
  (combinable-field t-combinable-field
                    (sanitize sanitize-it)
                    (combine  append)))

(define fragment
  (t-fragment
    (combinable-field-fragment '(fragment-value))))

(define instance (t
  (combinable-field '(base-value))
  (fragments        (list fragment))))

(t-combinable-field instance)
=> '(base-value fragment-value)

The fragment type is implicitly defined because at least one field contains a combine property. It contains one field for each combinable property. Note, however, that the fragment fields do not inherit the properties of the main type. In order to define properties on fragment fields they have to be added to the combine property:

(define-record-type <t> t make-t t? this-t
  (combinable-field t-combinable-field
                    (sanitize sanitize-it)
                    (combine  append (sanitize sanitize-it))))

For sanitizers inheriting the property might sometimes be desirable, but it sometimes might not be, so it doesn't make sense to do so automatically for all uses. Additionally, it is less clear that other properties like innate or thunked will want to be inherited.

Placing fragments at the end

Fragments are placed at the end of a struct definition:

(define-record-type* <t> t make-t t? this-t
  (field0 t-field0 (combine +))
  (field1 t-field1 (combine append)))

(define frag
  (t-fragment
    (field0 2)
    (field1 '(frag-val))))

(define inst
  (t
    (field0 3)
    (field1 '(inst-val))
    (fragments (list frag))))

This is a surprising decision because traditionally variable arguments (the field specifications) come at the end of an argument list. However, it is important to lay out the arguments this way in order to maintain visual intuitiveness when reading invocations.

Currently, record constructors contain two kinds of forms: an inherit form or a field form. The inherit form always goes at the top, followed by field forms. This is intuitive because the inherited value defines the default field values and the following forms override those values.

Placing the fragments form at the end keeps this visual intuition: we start with the parent values, override with the field values, then add in the fragment values.

Placing the fragments form at the top or middle would prompt us to start the process by adding the fragment values with the parent values, then override with the field values. But fragment values are combined with field values when they prevail. Placing the fragments form anywhere other than end would harm readability.

Random Decisions

The sanitizer for the main type runs once on the literally given value, then again on the combined value. It is not clear that this is the best choice, but removing sanitization from one of those places will not break any code (unless the sanitizer relies on side effects, which it should not) while adding the sanitizer to one of those places could break code.

I wanted to add the combination logic to wrap-field-value in order to minimize changes, but this caused a problem with running the sanitizer too many times. The problem is that in order to do it this way I would need to wrap the struct-ref call in record-inheritance, but this field was already sanitized when the instance created.

Fragment names are mostly determined by concatenating -fragment to the ends of the base type's names. For example, if the syntactic constructor is foo then the fragment's syntactic constructor will be foo-fragment. However, if the original name ends in a symbol then it will stay at the end so that foo? becomes foo-fragment? instead of foo?-fragment and <foo> becomes <foo-fragment>. This should probably be expanded to be any number of symbols, so than <foo!> would become <foo-fragment!> instead of <foo!-fragment>.

Arguments are ordered as base-value fragment1-value ... where the fragment values are in the same order that they appear in the fragment list. I think it will be useful to make this order guaranteed instead of incidental.

Technically, fragment fields can themselves have combine properties. This seems like a code smell, but I dislike categorically disallowing something just because it "seems like a bad idea to me". There might be a circumstance where it makes sense. Disallowing it would imply that I know better than every other programmer who will ever exist, even though I have no knowledge of their circumstances.

Download the markdown source and signature.