On this page:
prefab-struct?
immutable-prefab-struct?
struct-easy
struct-predicate
struct-accessor-by-name
istruct/  c
tupler?
tupler-length
tupler/  c
tupler-pred?-fn
tupler-ref-fn
tupler-proj-fns
tupler-make-fn
define-imitation-simple-struct
define-syntax-and-value-imitation-simple-struct
auto-write
auto-equal
define-imitation-simple-generics

8 Utilities for Structs🔗ℹ

 (require lathe-comforts/struct)
  package: lathe-comforts-lib

procedure

(prefab-struct? v)  boolean?

  v : any/c
Returns whether the given value is an instance of a prefab struct type.

procedure

(immutable-prefab-struct? v)  boolean?

  v : any/c
Returns whether the given value is an instance of a prefab struct type and has no mutable fields.

syntax

(struct-easy (name-id slot-id ...) rest ...)

This is a convenience layer over struct, which has some features to automate the implementation of the struct’s print behavior and equal? behavior.

The interface is rather unstable at this point. It’s not even clear at this point which pieces of the behavior here are by design and which are temporary kludges.

Here are some non-normative examples of how it can be used:

The definition (struct-easy (my-data field-one field-two)) behaves the same way as (struct my-data (field-one field-two)) except that it also implements a gen:custom-write behavior based on make-constructor-style-printer.

In the rest section, the first occurrence of the #:other keyword begins passing the rest of the subforms through to the struct form. To illustrate, this means the definition (struct-easy (my-data field-one field-two) #:other ...) behaves the same way as (struct my-data (field-one field-two) ...), aside from that custom write behavior.

If #:equal appears in the rest section (before any occurrence of #:other), then an implementation of gen:equal+hash will be generated so that the resulting structure type’s instances are equal? if their corresponding fields are equal?.

If a list of the form (#:guard-easy body-expr ...) appears in the rest section (before any occurrence of #:other), then the resulting structure type runs each body-expr when an instance is constructed. In those expressions, local variables corresponding to each field (e.g. field-one and field-two) are bound to the values that instance is being constructed with. The results of each body-expr are ignored; they’re expected to raise exceptions if the field values are unacceptable.

If #:write followed by an expression appears, then whenever an instance of this structure type would have its gen:custom-write behavior invoked, it runs that expression with local variables in scope corresponding to each field name (e.g. field-one and field-two), each bound to the instance’s respective field value. The expression is expected to return a procedure that takes the instance value itself and returns a list of values to be printed. (Since these are redundant, typically the expression will either ignore the field variables or ignore the instance value.) The resulting list of values is printed according to the behavior of make-constructor-style-printer.

If #:error-message-phrase followed by an expression appears, then that expression will be evaluated as the structure type is defined, and its result will be used in certain error messages of the generated gen:custom-write and gen:equal+hash implementations, particularly the errors that occur when these methods are called with a value that isn’t actually an instance of this structure type. The errors are of the form "expected <this> to be <phrase>", and the default "<phrase>" is "an instance of the <name> structure type". For certain structure types, it may make sense to use #:error-message-phrase to change this to a more pithy phrase, like "an RGB color" rather than "an instance of the rgb-color structure type." (However, is it even possible for clients of struct-easy to observe these errors in their programs? This feature might not make any visible difference.)

syntax

(struct-predicate struct-name-id)

Expands to the predicate identifier associated with the given structure type name.

For instance, if a struct is defined as (struct my-data (field-one field-two)), then traditionally we can recognize instances of the struct using (my-data? x), and now we can also recognize them using ((struct-predicate my-data) x).

This comes in handy mostly when defining other syntax transformers that deal with structure type names. Sometimes it allows those syntax transformers to be written using simple syntax templates, saving the trouble of making manual calls to syntax-local-value and extract-struct-info.

syntax

(struct-accessor-by-name struct-name-id field-name-id)

Expands to the struct field accessor identifier associated with the given structure type name and field name.

For instance, if a struct is defined as (struct my-data (field-one field-two)), then traditionally we can extract the first field using (my-data-field-one x), and now we can also extract it using ((struct-accessor-by-name my-data field-one) x).

This comes in handy mostly when defining other syntax transformers that deal with structure type names. Sometimes it allows those syntax transformers to be written using simple syntax templates, saving the trouble of making manual calls to syntax-local-value and extract-struct-info.

syntax

(istruct/c name-id field/c-expr ...)

 
  field/c-expr : contract?
Returns a contract that recognizes an instance of structure type name-id where the fields abide by the respective field/c-expr contracts.

Unlike struct/c (but like match/c), this works even when name-id is an immutable structure type name and the field/c-expr contracts contain one or more impersonator contracts.

However, this comes at the price of some quirks. This operation works by reconstructing the struct altogether when a higher-order projection is taken. This means the projection of this struct isn’t necessarily eq?, equal?, or impersonator-of? to the original value. In fact, the projection becomes an instance of the structure type name-id, even when the original value is an instance of a distinct structure subtype of name-id.

procedure

(tupler? v)  boolean?

  v : any/c
Returns whether the given value is a tupler.

A tupler is an object that carries the constructor, predicate, and positional accessor functionality of an immutable tagged tuple type that’s distinct from the type manipulated by any other tupler. Essentially, a tupler carries most of the functionality an immutable struct definition creates except for its structure type descriptor. A tupler can only be created by define-syntax-and-value-imitation-simple-struct.

procedure

(tupler-length t)  natural?

  t : tupler?
Returns the number of projections the given tupler has. This is the number of projections can be accessed from a tuple value and the number of projection values that must be supplied to the tupler’s constructor.

procedure

(tupler/c length/c)  flat-contract?

  length/c : flat-contract?
Returns a flat contract that recognizes a tupler whose length abides by the given flat contract.

procedure

((tupler-pred?-fn t) v)  boolean?

  t : tupler?
  v : any/c
Returns whether the given value is recognized by the given tupler as one of its tuple values.

procedure

((tupler-ref-fn t) v i)  any/c

  t : tupler?
  v : (tupler-pred?-fn t)
  i : (and/c natural? (</c (tupler-length t)))
Returns the projection at the given index in the given tuple value as observed by the given tupler.

procedure

(tupler-proj-fns t)  (listof (-> (tupler-pred?-fn t) any/c))

  t : tupler?
Returns a list of projection procedures based on the given tupler.

procedure

((tupler-make-fn t) proj ...)  (tupler-pred?-fn t)

  t : tupler?
  proj : any/c
Constructs a tuple value using the given tupler. There should be as many proj arguments as the tupler’s length.

syntax

(define-imitation-simple-struct
  (inst?-id inst-field-id ...)
  inst-id
  reflection-name-expr inspector-expr option ...)

syntax

(define-syntax-and-value-imitation-simple-struct
  (inst?-id inst-field-id ...)
  inst-id
  tupler-id
  reflection-name-expr inspector-expr option ...)
 
option = (#:prop prop-expr prop-val-expr)
  | (#:gen gen:name method-definition ...)
  | (auto-write)
  | (auto-equal)
 
  inspector-expr : (or/c inspector? #f 'prefab)
  reflection-name-expr : symbol?
  prop-expr : struct-type-property?
Creates a new structure type, and defines struct-like operations which construct and deconstruct an actual structure value of that type.

The variable inst?-id is defined to be a predicate that detects these structures.

Each variable inst-field-id is defined to be a procedure that accesses the corresponding positional field of any given one of these structures.

The variable inst-id is defined to be a match expander that can be used to construct or match these structures.

The variable tupler-id (when using define-syntax-and-value-imitation-simple-struct) is defined to be a first-class tupler object that can be used to construct or match these structures.

The structure type is created with the given reflection name, the given inspector, a number of fields equal to the number of inst-field-id variables specified, and structure type property bindings determined by the given option entries.

The reflection name is used for certain reflective operations.

If the inspector is the value 'prefab, this accesses a prefab structure type instead of creating a new one, and there must be no option entries specified.

An option of the form (#:prop prop-expr prop-val-expr) is like providing the struct option #:property prop-expr prop-val-expr. It specifies a binding of the prop-expr structure type property to the value prop-val-expr.

An option of the form (#:gen gen:name method-definition ...) is like providing the struct option #:methods gen:name [method-definition ...]. The gen:name must be an identifier with a transformer binding that specifies a generic interface as defined by define-generics. The method-definition forms define the implementation of this interface’s methods for this new structure type just as they do in struct.

An option of the form (auto-write) specifies implementations of gen:custom-write and prop:custom-print-quotable which use make-constructor-style-printer to display the value.

An option of the form (auto-equal) specifies an implementation of gen:equal+hash which treats any two of these structures as equal? if their fields are equal?.

Note that unlike struct, this has no support for creating structure types that have supertypes, subtypes, guard procedures, mutable fields, or automatic fields. For the most part, all these features except supertypes and subtypes can be simulated: Mutable fields can be simulated with immutable fields that contain mutable boxes, while guard procedures and automatic fields can be simulated by defining another procedure to call instead of calling the defined constructor directly.

To have more complete control over the structure type created, use struct. However, that will not produce a tupler value.

structure type property expander

(auto-write)

A syntax which is only useful as an option to define-imitation-simple-struct and define-syntax-and-value-imitation-simple-struct. In that context, it specifies that the created structure type should have implementations of gen:custom-write and prop:custom-print-quotable which use make-constructor-style-printer to display a structure value.

structure type property expander

(auto-equal)

A syntax which is only useful as an option to define-imitation-simple-struct and define-syntax-and-value-imitation-simple-struct. In that context, it specifies that the created structure type should have an implementation of gen:equal+hash which treats any two of the structure values as equal? if their fields are equal?.

syntax

(define-imitation-simple-generics
  inst?-id inst-impl?-id
  (#:method method-id () ... (#:this) () ...)
  ...
  prop:inst-id build-inst-impl-id
  prop-name-expr inst-impl-reflection-name-expr supers-expr)
 
  prop-name-expr : symbol?
  inst-impl-reflection-name-expr : symbol?
  supers-expr : (listof (cons/c struct-type-property? (-> inst-impl? any/c)))
Creates a new structure type property, creates a new structure type for use as the type of implementations of that property, and defines generic-interface-like operations for using these things.

The variable inst?-id is defined to be a predicate which determines whether the given value implements the property. Usually a value is considered to implement the property if its structure type has an implementation associated with the property. A structure type descriptor is special; it’s considered to implement the property if the structure type it’s a descriptor for has an implementation associated with the property.

The variable inst-impl?-id is defined to be a predicate which recognizes values of the structure type created for representing implementations of this property.

Each variable method-id is defined to be a procedure of the indicated number of arguments. Each () occurrence represents one argument, and the occurrence of (#:this) represents one argument which receives special treatment. The argument in the (#:this) position is expected to be a value which implements the property. Its implementation for the property determines the rest of the procedure’s behavior.

The variable prop:inst-id is defined to a structure type property descriptor. When a structure type has an implementation associated with the descriptor prop:inst-id, that implementation is also associated with the property the rest of these operations interact with. (In fact, for now this is the descriptor of the property itself.)

The variable build-inst-impl-id is defined to be a procedure which constructs a value which represents an implementation of this property from N arguments, where N is the number of #:method clauses in this definition. Each argument must be an implementation to use for the corresponding method.

The symbol resulting from prop-name-expr is used for certain reflective operations on the structure type property descriptor.

The symbol resulting from inst-impl-reflection-name-expr is used for certain reflective operations on instances of the structure type of implementations.

The list resulting from supers-expr has the same meaning as the supers argument to make-struct-type-property. It’s an association list mapping a structure type property to a function which transform the implementation of this property into the implementation of that one. When a structure type has an implementation of this property, it has an implementation of all of those as well, generated by the given functions.

When composing a supers argument to pass in, keep in mind that the values representing the implementation of this property are very opaque. Usually the only way to use them is by incorporating them into the creation of another structure type. Most supers arguments will simply use constant values or call the generic methods, ignoring the implementation value they receive altogether.

Note that unlike define-generics, this has no support for creating generic interface information that can be used with the struct #:method syntax; it has no support for giving methods optional arguments, keyword arguments, rest arguments, or non-dispatching fast paths; it has no support for letting instances implement some methods but not others; and it does not define a contract combinator.

Some of these drawbacks are tricky to work around, but not all of them. Methods which take complex argument lists or which have fast paths can be defined as wrapper procedures. The idea of an unimplemented method can be approximated by adding a second method that returns a boolean saying whether the first is implemented.

The #:method syntax is not especially feasible. It’s possible to use it by writing a separate define-generics interface with a #:derive-property option that populates this property. However, users should be discouraged from calling that wrapper interface’s methods directly or using its contract combinator, since those things will look at the define-generics interface, not this property it’s intended to populate. Instead of using the #:method syntax, it’s recommended to define various procedures and macros that make calls to build-inst-impl-id with the desired combinations of functionality.

Even the contract combinator can be simulated in a more big-picture sense: Higher-order contracts for structure type properties will tend to need to replace a property’s implementation with an instrumented version. Not all structure types that implement a properly will do so in a way that’s easily decoupled from their other functionality, much less swapped out for another whole implementation. That means the ability to swap something out is essentially another method that structure type can implement, and it can be designed as such.

When writing contracts for values whose design isn’t easy to change, another approach is to intercept those values at the boundaries of a program and convert them into a custom structure type whose design is easier to control.