Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

struct-plus-plus module #18

Open
dstorrs opened this issue Aug 26, 2021 · 1 comment
Open

struct-plus-plus module #18

dstorrs opened this issue Aug 26, 2021 · 1 comment

Comments

@dstorrs
Copy link

dstorrs commented Aug 26, 2021

struct-plus-plus can be found here: https://pkgs.racket-lang.org/package/struct-plus-plus

It allows for creation of struct types that come with: keyword constructors, per-field contracts and wrapper functions, dotted accessors, auto-generated functions to convert to/from the struct, functional setters and updaters, dependency checking among fields, easy introspection, and reflection.

Simple Example:

#lang racket

(require struct-plus-plus)

(struct++ person
          ([name (not/c (curry equal? "")) ~a]
           [(age +nan.0) (or/c +nan.0 positive?)])
          (#:rule ("no eugenes"
                   #:check (name) [(not (equal? "eugene" (string-downcase (~a name))))])
           #:convert-from (vector (vector? (vector name age) (name age)))
           )
          #:transparent)

(define alice (person++ #:name "alice" #:age 18))
(display "manual creation: ") alice
(display "converted from vector: ") (vector->person++ (vector "alice" 18))
(person.name alice)
(person++ #:name "bob")
(person++ #:name 'tom #:age 83)
(person++ #:name "eugene")

Running this produces:

manual creation: (person "alice" 18)
converted from vector: (person "alice" 18)
"alice"
(person "bob" +nan.0)
(person "tom" 83)
; failed in struct++ rule named 'no eugenes' (type: check): check failed                       
;   name: "eugene"                                                                             
; Context:                                                                                     
;  /Users/dstorrs/Library/Racket/8.0/pkgs/struct-plus-plus/main.rkt:328:10                     


Code

The following code is a compilation of two files:

;;========   main.rkt  =========

#lang racket/base

(require racket/require
         (multi-in handy (hash struct))
         (multi-in racket (bool contract/base contract/region function match promise))
         (only-in racket/list count flatten)
         "reflection.rkt"

         (for-syntax racket/base
                     (only-in racket/list partition)
                     racket/syntax
                     syntax/parse
                     syntax/parse/class/struct-id
                     syntax/parse/experimental/template)
         )

(provide struct++ struct->hash (all-from-out "reflection.rkt"))

;;======================================================================


(begin-for-syntax

  ; Set up various syntax classes and metafunctions.  struct++ itself
  ; is defined below this begin-for-syntax


  ;;    syntax->keyword was lifted from:
  ;; http://www.greghendershott.com/2015/07/keyword-structs-revisited.html
  (define syntax->keyword (compose1 string->keyword symbol->string syntax->datum))

  ;;--------------------------------------------------

  (define-template-metafunction (make-dotted-accessor stx)
    (syntax-parse stx
      [(make-dotted-accessor #f _ _ _ _ _ _)
       #''()]
      [(make-dotted-accessor #t
                             struct-id ctor-id predicate
                             field-name field-contract wrapper)
       (with-syntax ([accessor-name (format-id #'struct-id
                                               "~a-~a"
                                               #'struct-id
                                               #'field-name)]
                     [dotted-accessor-name (format-id #'struct-id
                                                      "~a.~a"
                                                      #'struct-id
                                                      #'field-name)])
         (template (define dotted-accessor-name accessor-name)))]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-functional-setter stx)
    (syntax-parse stx
      [(make-functional-setter #f _ _ _ _ _ _)
       #''()]
      [(make-functional-setter #t
                               struct-id ctor-id predicate
                               field-name field-contract wrapper)
       (with-syntax ([setter-name (format-id #'struct-id
                                             "set-~a-~a"
                                             #'struct-id
                                             #'field-name)])
         (template
          (define/contract (setter-name instance val)
            (-> predicate field-contract predicate)
            (hash->struct/kw ctor-id
                             (safe-hash-set (struct->hash struct-id instance)
                                            'field-name
                                            (wrapper val))))))]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-functional-updater stx )
    (syntax-parse stx
      [(make-functional-updater #f
                                struct-id ctor-id predicate
                                field-name field-contract wrapper)
       #''()
       ]
      [(make-functional-updater #t
                                struct-id ctor-id predicate
                                field-name field-contract wrapper)
       (with-syntax ([updater-name (format-id #'struct-id
                                              "update-~a-~a"
                                              #'struct-id
                                              #'field-name)]
                     [getter (format-id  #'struct-id
                                         "~a-~a"
                                         #'struct-id
                                         #'field-name)]
                     )
         (template
          (define/contract (updater-name instance updater)
            (-> predicate (-> field-contract field-contract) predicate)
            (hash->struct/kw ctor-id
                             (safe-hash-set (struct->hash struct-id instance)
                                            'field-name
                                            (wrapper (updater (getter instance))))))))]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-convert-for-function-name stx)
    (syntax-parse stx
      [(make-convert-for-function-name struct-id purpose)
       (format-id #'struct-id "~a/convert->~a" #'struct-id #'purpose)]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-convert-for-function stx)
    (syntax-parse stx
      [(make-convert-for-function  struct-id purpose predicate arg ...)
       (template
        (define/contract ((make-convert-for-function-name struct-id purpose) instance)
          (-> predicate any)
          (hash-remap (struct->hash struct-id instance) (~@ arg ...))))]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-accessor-name stx)
    (syntax-parse stx
      [(make-accessor-name struct-name field-name)
       (format-id #'struct-name "~a-~a" #'struct-name #'field-name)]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-field-struct stx)
    (syntax-parse stx
      [(make-field-struct struct-name field-name contract wrapper default)
       #'(struct++-field ('field-name
                          (make-accessor-name struct-name field-name)
                          contract
                          wrapper
                          default))]))

  ;;--------------------------------------------------

  (define-template-metafunction (make-convert-from-function stx)
    (syntax-parse stx
      [(make-convert-from-function struct-id:id name:id source-predicate:expr
                                   match-clause:expr (f:field ...))
       (with-syntax ([func-name (format-id #'struct-id "~a->~a++" #'name #'struct-id)]
                     [struct-predicate (format-id #'struct-id "~a?" #'struct-id)]
                     [ctor (format-id #'struct-id "~a++" #'struct-id)]
                     [((ctor-arg ...) ...) #'(f.ctor-arg ...)])
         (template
          (define/contract (func-name val)
            (-> source-predicate struct-predicate)
            (match val
              [match-clause (ctor ctor-arg ... ...)]))))]))

  ;;----------------------------------------------------------------------

  (define-template-metafunction (make-ctor-contract stx)
    (define-syntax-class contract-spec
      (pattern (required?:boolean  (name:id contr:expr))))
    ;;
    (syntax-parse stx
      #:datum-literals (make-ctor-contract)
      [(make-ctor-contract (item:contract-spec ...+ predicate))
       (let-values
           ([(mandatory optional)
             (partition (syntax-parser [(flag _) (syntax-e #'flag)])
                        (map (syntax-parser [(flag (name contr))
                                             (quasitemplate (flag (#,(syntax->keyword #'name)
                                                                   contr)))])
                             (syntax->list #'(item ...))))])
         (with-syntax ((((_ (mand-kw mand-contract)) ...) mandatory)
                       (((_ (opt-kw  opt-contract)) ...)  optional))
           (template (->* ((?@ mand-kw mand-contract) ...)
                          ((?@ opt-kw opt-contract) ...)
                          predicate))))]))

  ;;--------------------------------------------------

  (define-syntax-class field
    (pattern (~or id:id
                  [id:id (~optional (~seq cont:expr (~optional wrap:expr)))])
             #:with required? #'#t
             #:with field-contract (template (?? cont any/c))
             #:with wrapper (template (?? wrap identity))
             #:with ctor-arg #`(#,(syntax->keyword #'id) id)
             #:with def #''no-default-given)

    (pattern [(id:id default-value:expr)
              (~optional (~seq cont:expr (~optional wrap:expr)))]
             #:with required? #'#f
             #:with field-contract (template (?? cont any/c))
             #:with wrapper (template (?? wrap identity))
             #:with ctor-arg #`(#,(syntax->keyword #'id) [id default-value])
             #:with def (template  default-value)
             )
    )

  ;;--------------------------------------------------

  (define-splicing-syntax-class rule
    (pattern
     (~seq #:rule (rule-name:str (~seq #:transform target (var:id ...) [code:expr ...+])))
     #:with type #''transform
     #:with result (template (set! target ((lambda (var ...) code ...) var ...))))

    (pattern
     (~seq #:rule (rule-name:str (~seq #:check (var:id ...) [code:expr])))
     #:with type #''check
     #:with result (template
                    ((lambda (var ...)
                       (when (not code)
                         (let ([args (flatten (map list
                                                   (map symbol->string '(var ...))
                                                   (list var ...)))])
                           (apply raise-arguments-error
                                  (string->symbol (format "failed in struct++ rule named '~a' (type: check)" rule-name))
                                  "check failed"
                                  args))))
                     var ...)))
    (pattern
     (~seq #:rule
           (rule-name:str (~seq #:at-least
                                min-ok:exact-positive-integer
                                (~optional predicate:expr)
                                (var:id ...))))
     #:with type #''at-least
     #:with result (template
                    (let* ([pred (?? predicate (procedure-rename
                                                (negate false?)
                                                'true?))]
                           [num-valid (count pred (list var ...))])
                      (when (< num-valid min-ok )
                        (let ([args (flatten (map list
                                                  (map symbol->string '(var ...))
                                                  (list var ...)))])
                          (apply raise-arguments-error
                                 (string->symbol (format "failed in struct++ rule named '~a' (type: at-least)" rule-name))
                                 "too many invalid fields"
                                 "minimum allowed" min-ok
                                 "predicate" pred
                                 args)))))))

  ;;--------------------------------------------------

  (define-splicing-syntax-class converter
    (pattern (~seq #:convert-for (name (opt ...)))))

  ; e.g. #:convert-from (db-row (vector? (vector a b c) (a b c)))
  (define-splicing-syntax-class convert-from-clause
    (pattern (~seq #:convert-from (name:id (source-predicate:expr
                                            match-clause:expr
                                            (f:field ...+))))))

  ;;--------------------------------------------------

  (define-splicing-syntax-class make-setters-clause
    (pattern (~seq #:make-setters? yes?:boolean)))

  ;;--------------------------------------------------

  (define-splicing-syntax-class make-dotted-accessors-clause
    (pattern (~seq #:make-dotted-accessors? yes?:boolean)))

  )

(define-syntax struct->hash
  (syntax-parser
    [(_ s:struct-id instance:expr)
     (template
      (let* ([name-str (symbol->string  (object-name s.constructor-id))]
             [field-name (lambda (f)
                           (string->symbol
                            (regexp-replace (pregexp (string-append  name-str "-"))
                                            (symbol->string (object-name f))
                                            "")))]
             )
        (make-immutable-hash (list  (cons  (field-name s.accessor-id)
                                           (s.accessor-id instance)
                                           ) ...))))]))

;;======================================================================

(define-syntax (struct++ stx)
  (syntax-parse stx
    ((struct++ struct-id:id
               (field:field ...)
               (~optional ((~alt (~optional make-setters:make-setters-clause)
                                 (~optional make-dotted-accessors:make-dotted-accessors-clause)
                                 (~optional (~and  #:omit-reflection omit-reflection))
                                 ;converters:converter-list
                                 c:converter
                                 cfrom:convert-from-clause
                                 r:rule)
                           ...))
               opt ...)
     #:with ctor-id   (format-id #'struct-id "~a++" #'struct-id)
     #:with predicate (format-id #'struct-id "~a?" #'struct-id)
     #:with reflectance-data (if (attribute omit-reflection)
                                 #'()
                                 #'(#:property prop:struct++
                                    (delay
                                      (struct++-info++
                                       #:base-constructor struct-id ; base struct constructor
                                       #:constructor ctor-id   ; struct-plus-plus constructor
                                       #:predicate predicate
                                       #:fields (list (struct++-field++
                                                       #:name     'field.id
                                                       #:accessor (make-accessor-name
                                                                   struct-id
                                                                   field.id)
                                                       #:contract field.field-contract
                                                       #:wrapper  field.wrapper
                                                       #:default  field.def)
                                                      ...)
                                       #:rules
                                       (list (~? (~@ (struct++-rule++
                                                      #:name r.rule-name
                                                      #:type r.type)
                                                     ...)))
                                       #:converters
                                       (list
                                        (~? (~@ (make-convert-for-function-name
                                                 struct-id
                                                 c.name)
                                                ...)))))))
     ; A double ... (used below) flattens one level
     (with-syntax* ([((ctor-arg ...) ...) #'(field.ctor-arg ...)])
       (quasitemplate
        (begin
          (struct struct-id (field.id ...) opt ... (~@ . reflectance-data))
          ;
          (define/contract (ctor-id ctor-arg ... ...)
            (make-ctor-contract
             ((field.required? (field.id field.field-contract)) ... predicate))

            (?? (?@ r.result ...))

            (struct-id (field.wrapper field.id) ...)
            )
          ;
          (?? (?@ (make-convert-for-function struct-id c.name predicate c.opt ...) ...))
          ;
          (?? (?@ (make-convert-from-function struct-id
                                              cfrom.name
                                              cfrom.source-predicate
                                              cfrom.match-clause
                                              (cfrom.f ...)) ...))

          ;
          (begin
            (make-dotted-accessor (?? make-dotted-accessors.yes? #t)
                                  struct-id ctor-id predicate
                                  field.id
                                  field.field-contract
                                  field.wrapper
                                  )
            ...)
          (begin
            (make-functional-setter (?? make-setters.yes? #t)
                                    struct-id ctor-id predicate
                                    field.id
                                    field.field-contract
                                    field.wrapper
                                    )
            ...)
          (begin
            (make-functional-updater (?? make-setters.yes? #t)
                                     struct-id ctor-id predicate
                                     field.id
                                     field.field-contract
                                     field.wrapper
                                     )
            ...)))))))

;;========   reflection.rkt  =========

#lang racket

(provide (struct-out struct++-rule)
         (struct-out struct++-field)
         (struct-out struct++-info)
         struct++-info++
         struct++-field++
         struct++-rule++
         prop:struct++ struct++? struct++-ref)

(struct struct++-rule  (name type))
(struct struct++-field (name accessor contract wrapper default))
(struct struct++-info
  (base-constructor constructor predicate fields rules converters))

(define-values (prop:struct++ struct++? struct++-ref)
  (make-struct-type-property 'struct++ 'can-impersonate))

;;----------------------------------------------------------------------

(define/contract (struct++-rule++  #:name name #:type type)
  (-> #:name string? #:type (or/c 'at-least 'transform 'check)
      struct++-rule?)
  (struct++-rule name type))

;;----------------------------------------------------------------------

(define/contract (struct++-field++
                  #:name           name
                  #:accessor       accessor
                  #:contract       [field-contract any/c]
                  #:wrapper        [wrapper        identity]
                  #:default        [default        'no-default-given])
  (->* (#:name        symbol?
        #:accessor    (-> any/c any/c))
       (#:contract    contract?
        #:wrapper     procedure?
        #:default     any/c)
       struct++-field?)
  (struct++-field name accessor field-contract wrapper default))

;;----------------------------------------------------------------------

(define/contract (struct++-info++
                  #:base-constructor base-constructor
                  #:constructor constructor
                  #:predicate predicate
                  #:fields fields
                  #:rules rules
                  #:converters converters)
  (-> #:base-constructor    procedure?
      #:constructor         procedure?
      #:predicate           predicate/c
      #:fields              (listof struct++-field?)
      #:rules               (listof struct++-rule?)
      #:converters          (listof procedure?)
      struct++-info?)

  (struct++-info base-constructor constructor predicate fields rules converters))

@spdegabrielle
Copy link
Contributor

Thank you David @dstorrs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants