struct+  +
1 Introduction
2 Synopsis
3 Syntax
4 Rules
5 Warnings, Notes, and TODOs
7.2

struct++

David K. Storrs

 (require struct-plus-plus) package: struct-plus-plus

1 Introduction

"struct-plus-plus" provides extended syntax for creating structs. It does not support field options (#:auto and #:mutable for individual fields), although those will be added. Aside from that, it’s a drop-in replacement for the normal "struct" form. So long as your struct does not use field options, you can literally just change "struct" to "struct++" and your code will continue to work as before but you will now have a keyword constructor and functional setters for all the fields.

"struct-plus-plus" offers the following benefits over normal "struct":

2 Synopsis

Let’s make a struct that describes a person who wants to join the military.

(struct++ recruit ([name           (or/c symbol? non-empty-string?) ~a]
                   [age            positive?]
                   [(height-m #f)  positive?]
                   [(weight-kg #f) positive?]
                   [(bmi #f)       positive?]
                   [(felonies 0)   exact-nonnegative-integer?]
                   [(notes "")])
 
          #:transparent)

> (define bob (recruit++ #:name      'bob

                         #:age       16

                         #:height-m  2

                         #:weight-kg 100))

> bob

(recruit "bob" 16 2 100 #f 0 "")

 

> (set-recruit-age bob 18)

(recruit "bob" 18 2 100 #f 0 "")

 

> (recruit++ #:name 'tom)

application: required keyword argument not supplied

procedure: recruit++

required keyword: #:age

Note about constructors:

There are two constructors for the "recruit" datatype: "recruit" and "recruit++". "struct++" will generate both of these while Racket’s builtin "struct" generates only "recruit". Only "recruit++" has keywords, contracts, etc. Using the default constructor will allow you to create structures that are invalid under the field contracts. See below:

> (recruit 'tom -3 99 10000 0.2 -27 'note)

(recruit 'tom -3 99 10000 0.2 -27 'note)

 

> (recruit++ #:name 'tom #:age -3 #:height-m 99 #:weight-kg 10000 #:bmi 0.2 #:felonies -27 #:notes 'note)

recruit++: contract violation

  expected: positive?

  given: -3

  in: the #:age argument of

      (->*

       (#:age

        positive?

        #:name

        (or/c symbol? non-empty-string?))

       (#:bmi

        positive?

        #:felonies

        natural?

        #:height-m

        positive?

        #:notes

        any/c

        #:weight-kg

        positive?)

       recruit?)

  contract from: (function recruit++)

  blaming: top-level

   (assuming the contract is correct)

3 Syntax

(struct++ type:id (field ...) rules struct-option ...)

 

 field :    field-id

          | (field-id                   field-contract               )

          | (field-id                   field-contract   wrapper     )

          | ((field-id  default-value)                               )

          | ((field-id  default-value)  field-contract               )

          | ((field-id  default-value)  field-contract   wrapper     )

 

 field-contract : contract? = any/c

 

 rules     :

             | (rule ...)

 

 rule      :   #:rule (rule-name #:at-least N pred (field-id ...+))

             | #:rule (rule-name #:check (field-id ...+) [code])

             | #:rule (rule-name #:transform field-id (field-id ...+) (code ...))

 

 rule-name :  string?

 

 pred      :

             | (-> any/c boolean?) = (negate false?)

 

 code      : <expression>

 

 struct-option : As per the 'struct' builtin. (#:transparent, #:guard, etc)

Note that supertypes are not supported as of this writing. The setter functions generated by "struct-plus-plus" make use of struct-copy, which doesn’t work reliably when dealing with supertypes. See Alexis King’s module "struct-update" for more details.

4 Rules

Structs always have business logic associated with them – that’s the entire point. Much of that can be embodied as contracts or wrapper functions, but if you want to enforce requirements between fields then you need rules. No one wants to code all that stuff manually, so let’s have some delicious syntactic sugar that lets us create them declaratively.

Let’s go back to our example of the recruit. In order to be accepted into the military, you must be at least 18 years of age, have no felonies on your record, and be reasonably fit (BMI no more than 25).

Bob really wants to join the military, and he’s willing to lie about his age to do that.

(define (get-min-age) 18.0)
(struct++ lying-recruit
          ([name (or/c symbol? non-empty-string?) ~a]
           [age positive?]
           [(height-m #f) (between/c 0 3)]
           [(weight-kg #f) positive?]
           [(bmi #f) positive?]
           [(felonies 0) exact-positive-integer?])
 
           (#:rule ("bmi can be found" #:at-least  2           (height-m weight-kg bmi))
            #:rule ("ensure height-m"  #:transform   height-m  (height-m weight-kg bmi) [(or height-m (sqrt (/ weight-kg bmi)))])
            #:rule ("ensure weight-kg" #:transform   weight-kg (height-m weight-kg bmi) [(or weight-kg (* (expt height-m 2) bmi))])
            #:rule ("ensure bmi"       #:transform   bmi       (height-m weight-kg bmi) [(or bmi (/ 100 (expt height-m 2)))])
            #:rule ("lie about age"    #:transform   age       (age) [(define min-age (get-min-age))
                                                                      (cond [(>= age 18) age]
                                                                            [else min-age])])
            #:rule ("eligible-for-military?" #:check           (age felonies bmi) [(and (>= age 18)
                                                                                        (= 0 felonies)
                                                                                        (<= 25 bmi))]))
         #:transparent)

 

> (define bob (lying-recruit++ #:name      'bob

                               #:age       16

                               #:height-m  2

                               #:weight-kg 100))

 

> bob

(lying-recruit "bob" 18.0 2 100 25 0)

Note that Bob’s name has been changed from a symbol to a string as per Army regulation 162.11a, his age has magically changed from 16 to 18.0, and his BMI has been calculated. Suppose we try to invalidate these constraints?

> (set-lying-recruit-age bob 3)

(lying-recruit "bob" 3 2 100 25 0)

Oops, the setters don’t respect the rules! That’s still TODO and will be coming out in the next release.

5 Warnings, Notes, and TODOs

Some of these were already mentioned above:

With great thanks to Greg Hendershott for his "Fear of Macros" essay, and to Alexis King for teaching me a lot about macros over email and providing the struct-update module which gave me a lot of inspiration.

And, as always, to the dev team who produced and maintains Racket. You guys rule.