How can I export the struct definition but not the constructor?

The goal is to have a struct type defined in one file and other files can use the struct (via accessors and match) but cannot create instances of the struct. Is there a way to do this aside from using #:constructor-name to separate the transformer binding and the constructor? This is relevant to code that I'm writing with struct-plus-plus.

; person.rkt
(provide get-person <X>) ; what should <X>? be?

(struct person (name))
(define (get-person db-id) ...query the database, return a `person` struct...)

; invitations.rkt
(require "person.rkt")
(match (get-person 7)  ; this works
  [(person name) name])

; this throws an error
(person 'bob)  ; error! constructor was not provided from person.rkt

What’s the reason you don’t want to use #:constructor-name. It sounds like a perfect solution for this.

But in case you really don’t want to use #:constructor-name, you can use the technique I described in https://racket.discourse.group/t/impersonate-syntax-transformer-cursed-or-not/971. Following results in a run-time error (which seems to be what you want -- but let me know if you want a compile-time error. I can do that too)

#lang racket

(module provider racket
  (require syntax/parse/define
           (for-syntax racket/struct-info))

  (provide get-person
           (rename-out [person* person]))

  (struct person (name))

  (define (get-person db-id)
    (person "foo"))

  (define-syntax-parse-rule (repack x:id constr new-id:id)
    (define-syntax new-id
      (impersonate-procedure
       (syntax-local-value #'x)
       (λ (y)
         (values (syntax-parser [(_ . args) #'(constr . args)]
                                [_:id #'constr])
                 y)))))

  (define better-person
    (procedure-rename
     (λ (name)
       (error "constructor was not provided"))
     'person))

  (repack person better-person person*))

(require 'provider)

(match (get-person 7)  ; this works
  [(person name) name])

; this throws an error
(person 'bob) ; constructor was not provided

1 Like

My struct-plus-plus module provides a very compact form for declaring a struct and a lot of scaffolding around it. Examples includes a keyword constructor which checks contracts and business logic, potentially transforms values, installs wrappers, etc. The problem is that I've never wanted to use different constructor names before so I didn't consider it when I was writing spp, meaning that right now it assumes the constructor name is the default value, so the code that it generates for the keyword constructor would end up failing.

(require struct-plus-plus)
(struct++ person ([name (or/c non-empty-string? symbol?) ~a]
                  [(age 18) positive?]) 
          #:transparent)

(person++ #:name "alice" #:age 27) ; => (person "alice" 27)
(person++ #:name 'bob)             ; => (person "bob" 18), because the ~a wrapper forced the name to string and age defaulted
(person++ #:name #f #:age 12)      ; => runtime error. name must be non-empty-string? or symbol?

(struct++ animal (species) ; contracts and wrappers are optional
          #:constructor-name make-animal
          #:transparent)

(animal++ #:species "dog")  
; /Users/dstorrs/test.rkt:17:10: animal: bad syntax;                                   
;  identifier for static struct-type information cannot be used as an expression                              
;   in: animal  

; fails because spp assumed that Racket's default constructor was 'animal'

I figured I'd ask if there's an easy way to do this before I started looking at rewriting parts of spp.

By default, a struct name plays two roles: a transformer that contains the structure's static info and its constructor. So off top of my head, I can't think of a way to do this without giving a different name to the constructor (via #:constructor-name) or the struct (via #:name).

1 Like