Defining the runtime value of a struct name

By default, the Racket struct macro creates two bindings for the name: a runtime binding, and a transformer binding. You can shift these around with #:name and #:constructor-name, but as far as I know, you can't supply the runtime name binding while keeping the transformer binding.

Here are some examples using a macro that simulates the ability to do this "correctly" (in my opinion of course):

racket> (struct* these (fields) #:name-expr (lambda args (these-constructor args)))
racket> (these 1 2 3)
(these '(1 2 3))

racket> (struct* singleton (a b) #:name-expr (singleton-constructor 'hello 'world))
racket> singleton
(singleton 'hello 'world)

To keep the examples clear and concise, the macro also defines #:constructor and #:transparent. Unfortunately, the macro also uses #:name which of course, breaks the interface to struct-out and pattern matching. If this could be achieved without using #:name, then I feel like it would solve the very common problem of the constructor being the wrong value for the struct's runtime name binding. The two issues to solve are:

  1. the name must still be exported by struct-out as a runtime binding.
  2. the name must still encapsulate information about the structure type as a transformer binding.

I actually feel like I run into this issue every time I define a set of structures in Racket.

--
Anthony Carrico

Here is simulated example using match:

racket> (match singleton ((struct singleton (a b)) a))
'hello

racket> (match (these 1 2 3) ((struct these (fields)) fields))
'(1 2 3)

Unfortunately this is required by the typical #:name work-around used by the struct* macro:

racket> (match singleton ((struct singleton-t (a b)) a))
'hello

racket> (match (these 1 2 3) ((struct these-t (fields)) fields))
'(1 2 3)

As I said, the struct* macro was just defined to simulate #:name-expr for the examples in this post, and is an no way a complete and correct struct alternate. Here is the definition:

(define-syntax (struct* stx)
  (syntax-parse stx
    ((_ name (field ...))
     (with-syntax ((transformer-id (format-id #'name "~a-t" (syntax-e #'name))))
       #'(struct name (field ...)
           #:name transformer-id
           #:transparent)))
    ((_ name (field ...) #:name-expr name-expr)
     (with-syntax ((transformer-id (format-id #'name "~a-t" (syntax-e #'name)))
                   (constructor-id (format-id #'name "~a-constructor" (syntax-e #'name))))
       #'(begin
           (struct name (field ...)
             #:name transformer-id
             #:constructor-name constructor-id
             #:transparent)
           (define name name-expr))))))

The pattern captured by the struct* macro is something I find myself doing by hand quite often when defining structs in Racket. Does the pattern look familiar to anyone else? And yet, it isn't a correct solution.

I'm quite confused about what things are not working here. You can definitely supply #:constructor-name while using the static binding, and you can supply both as well. Here's an example:

#lang racket

(let ()
  (struct x () #:constructor-name mk-x #:name X)

  (mk-x)

  (match (mk-x)
    [(X) 1]))

(let ()
  (struct x () #:constructor-name mk-x)

  (mk-x)

  (match (mk-x)
    [(x) 1]))

Can you clarify which names you wish were different in the example I posted?

Also, the use of #:name-expr in your example seems like it's about the constructor behavior, which seems unrelated to me but maybe I'm missing something.

1 Like

Sorry, I'll try to explain.

The purpose of #:name-expr is to provide the appropriate binding for the name in the runtime environment when the default value (the constructor) is inappropriate. This is important when creating an API, and I often have to use #:name to achieve the effect, but this is undesirable because it changes the struct's transformer binding so that cooperating macros like struct-out and match no longer work like normal.

I still don't get it. If the code doesn't supply #:name and only supply #:constructor-name, then it gets to name the constructor however it likes.

In the second example in samth's code, the ‘name’ of the struct x works in struct-out and match. What is different from your expectation in this example?

Do you want the structs to be printed differently?

Or, are you looking for custom/smart constructor and want to avoid exposing the one created by struct?

Sure, an abstract datatype like you suggest would be another good example. It really depends on your desired API. The point is that currently the most precious binding (the name of the structure) can't have any binding except the constructor.

I feel like I run into this issue all the time in Racket, so I was wondering if it is headache for others too, but judging by the reactions here, perhaps I'm the only one.

No, that is a different issue.

I wouldn't say it's caused me significant pain, but I have definitely run into this before, if I am understanding correctly.

A recent example is the API for a bag datatype I have been working on.

The struct describes what the bag is, but the construction of the bag itself is slightly more complicated, and I don't want to expose that to the user at all.

This seems to corroborate what @samth is saying about it being a "behavioural" pattern as opposed to "nominal" one.

So, I end up defining the struct, and a synthetic constructor, and then providing the synthetic constructor as a renamed version of the original constructor, without struct-out, which feels somewhat ad hoc.

...

(provide
 (rename-out
  [make-bag bag])
 bag-powers
 bag-primes
 bag-arity
 ...)

...

(struct bag [powers #;hash?
             primes #;set?
             arity  #;natural?]
  #:transparent
  #:property prop:custom-print-quotable 'never
  #:methods gen:custom-write
  [(define (write-proc self port mode)
     (fprintf port "#bag~a" (bag-factors self)))])

...

(define (make-bag . primes)
  (for/fold ([powers empty-hash]
             [primes empty-set]
             [arity  0]
             #:result (bag powers primes arity))        
            ([prime (in-list primes)])
    (values
     (hash-update powers prime add1 0)
     (set-add primes prime)
     (+ arity 1))))

Irrespective of what I call #:name and #:constructor-name (or even #:extra-name), I want to be able to expose to the user only one way to create a well-formed bag, without divorcing it from the struct itself too much. I do, nonetheless, want to expose accessors for the struct.

(I might be talking nonsense here due to inexperience, so add salt to taste).

The struct* semantics could be a nice way of abstracting this.

Looking at it from another point of view, perhaps it would be helpful to make the API for the struct-out provision more powerful, in that one can expose the names and so on more granularly. Of course, this only helps if the struct lives in a separate module to the code it will inhabit.


P.S. this made me think about structs a bit more than I usually do, so thanks for sharing.

1 Like

I don't think I completely understand how the two different bindings for a struct's name are generated in the standard struct macro. Here is an proposed fix for the output of the struct* macro on the singleton example:

;; begin proposed struct* expansion
(struct singleton (a b)
  #:name singleton-t
  #:constructor-name singleton-constructor
  #:transparent)
(define singleton (singleton-constructor 'hello 'world))
(define-for-syntax singleton (syntax-local-value #'singleton-t))
;; end proposed struct* expansion

It isn't quite right. Here is the output:

> singleton
(singleton 'hello 'world)

> (match singleton ((struct singleton (a b)) a))
racket-mode-repl:1:26: match: singleton does not refer to a structure definition
  at: singleton
  in: (struct singleton (a b))

So, yes, struct can't accept arbitrary user-defined function as constructors. What can be done is to write a custom macro that acts both as the struct-information binding and delegate to the custom constructor function.

  1. The protocol that struct and match use is called struct-info. Examples here show how to write ‘phantom’ structs that match understands: 5.7 Structure Type Transformer Binding
  2. The macro that works like ordinary variables can be conveniently created with make-variable-like-transformer. 5 Macro Transformer Helpers

Here is a piece of code that implements the core:

#lang racket/base

(require (for-syntax racket/base))

(begin-for-syntax
  (require racket/struct-info syntax/transformer)

  (struct id-with-struct-info (macro-proc info)
    #:property prop:procedure   (struct-field-index macro-proc)
    #:property prop:struct-info (lambda (self)
                                  (id-with-struct-info-info self))
    #:transparent)
  )

With this definition, for any struct-created type, the code can define a public interface that struct and match both understand (for, e.g., struct-out, defining substructs, and pattern matching):

(struct these (fields)
  #:transparent)

(define (these/restarg . args)
  (these args))

(require (for-syntax racket/list syntax/transformer))
(define-syntax these/public
  (id-with-struct-info (make-variable-like-transformer #'these/restarg)
                       (list-set (extract-struct-info (syntax-local-value #'these))
                                 1
                                 #'these/public)))

Here's some example uses of the public interface, these/public:

;; pattern matching
(require racket/match)
(match (these/public 1 2 3)
  [(these/public f)
   (printf "fields = ~s\n" f)])

;; subtypes
(struct those these/public (other-fields) #:transparent)
(those '(1 2 3) "abc")

;; exporting
(provide (struct-out these/public))
(module* main racket/base
  (require (submod "..") racket/match)
  these/public
  ; these ;; => unbound
  ; these/restarg ;; => unbound
  these?
  these-fields
  (match (these/public 'a 'b 'c 'd 'e)
    [(these/public fields) fields]))

Of course, the pattern that id-with-struct-info and these/public illustrate can be packaged up into a macro:

#lang racket/base

(require (for-syntax racket/base))

(begin-for-syntax
  (require racket/list racket/struct-info syntax/transformer)

  (struct customized-ctor (ctor-macro-proc struct-info)
    #:property prop:procedure   (struct-field-index ctor-macro-proc)
    #:property prop:struct-info (lambda (self)
                                  (customized-ctor-struct-info self))
    #:transparent)

  (define (setup-custom-ctor struct-id exported-ctor-id custom-ctor-id)
    (define info
      (extract-struct-info
       (syntax-local-value struct-id)))
    (customized-ctor (make-variable-like-transformer custom-ctor-id)
                     (list-set info 1 exported-ctor-id)))
  )

(require (for-syntax racket/syntax syntax/parse/pre))

(define-syntax (struct/ctor stx)
  (syntax-parse stx
    [(_ custom-ctor-name:id
        name:id
        (~optional super:id)
        (field ...)
        form ...)
     #:with internal-id (format-id #'here "internal:~a" #'name #:source #'name)
     #`(begin
         #,(syntax/loc stx
             (struct name (~? super) (field ...)
               #:name internal-id
               form ...))
         (define-syntax name
           (setup-custom-ctor (quote-syntax internal-id)
                              (quote-syntax name)
                              (quote-syntax custom-ctor-name))))]))


;; Example 0

(struct/ctor singleton-inst
             singleton (a b)
             #:constructor-name internal-singleton-ctor
             #:transparent)

(define singleton-inst
  (internal-singleton-ctor #t #f))

(printf "=========== example 0 ===========\n\n")

(require racket/match)
singleton
(match singleton
  [(singleton a-val b-val) (list a-val b-val)])




;; Example 1

(struct/ctor custom-these-ctor
             these (fields)
             #:constructor-name internal-these-ctor
             #:transparent)

(define (custom-these-ctor . args)
  (internal-these-ctor args))

(printf "\n\n\n=========== example 1 ===========\n\n")

;; pattern matching
(define t (these 1 2 3))
t
(match t
  [(these f)
   (printf "fields = ~s\n" f)])

;; subtypes
(struct those these (other-fields) #:transparent)
(those '(1 2 3) "abc")

;; exporting
(provide (struct-out these))
(module* main racket/base
  (require (submod ".."))
  these
  these?
  these-fields
  ; internal-these-ctor ;; => unbound
  ; custom-these-ctor ;; => unbound
  (these "A" "B" "C" "D" "E" "G" "H")
  )




;; Example 2

(printf "\n\n\n=========== example 2 ===========\n\n")

(struct S (x1) #:transparent)
(struct/ctor make-T
             T S (x2)
             #:constructor-name make-T
             #:transparent)
(struct/ctor U/auto-x
             U T (y [z #:mutable])
             #:constructor-name make-U
             #:transparent)

(define (U/auto-x y)
  (make-U "U/auto-x filler1" "U/auto-x filler2" y y))

(define u (U 34))
u
(S? u)
(T? u)
(set-U-z! u "mutated!")
(match u
  [(and (struct* T ([x2 x2-val]))
        (struct* U ([z z-val])))
   (printf "x2-val = ~s, z-val = ~s\n" x2-val z-val)])

(printf "\n\n\n=================================\n\n")
2 Likes

Ok, now I think I understand. For some structure types, there is some other distinguished runtime value that you want to be associated with the name of the struct, instead of the constructor (such as a singleton value or a smart constructor or something else). But currently, while struct lets you disconnect the struct name from the default constructor, it doesn't let you connect the struct name to that other value.

I think this would be a quite reasonable thing to add to struct. I would implement it by providing a different keyword argument (#:name-target is a bad idea but I'm not going to come up with a better one right now) and if you supply that, which must be an identifier, then the resulting static struct-info value renames to that instead of the constructor (and you have to supply #:constructor-name).

1 Like

Relevant: https://racket.discourse.group/t/impersonate-syntax-transformer-cursed-or-not/971

I can see that. I'm getting a little clearer on the mechanism. I'll study your code. Thank you.

Precisely.

Here's an example for your case:

#lang racket

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

  (provide define-repackaged)

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

(module these racket
  (require (submod ".." repackage))

  (provide (struct-out these))

  (struct these (fields) #:transparent
    #:name these/internal
    #:constructor-name these/internal)

  (define better-these
    (procedure-rename
     (λ args (these/internal args))
     'these))

  (define-repackaged these
    #:old-id these/internal
    #:constructor better-these))

(module singleton racket
  (require (submod ".." repackage))

  ;; not using struct-out here,
  ;; so the only way to access elements is through match
  (provide singleton)

  (struct singleton (a b) #:transparent
    #:name singleton/internal
    #:constructor-name singleton/internal)

  (define better-singleton
    (singleton/internal 'hello 'world))

  (define-repackaged singleton
    #:old-id singleton/internal
    #:constructor better-singleton))

(require 'these)
(these 1 2 3) ;=> (these '(1 2 3))
(these-fields (these 1 2 3)) ;=> '(1 2 3)
(these? (these 1 2 3)) ;=> #t
(match (these 1 2 3)
  [(these val) val]) ;=> '(1 2 3)
(struct-copy these (these 1 2 3) [fields '(3 4)]) ;=> (these '(3 4))

(require 'singleton)
singleton ;=> (singleton 'hello 'world)
(match singleton
  [(singleton a b) (format "~a ~a!" a b)]) ;=> "hello world!"

Yes, very relevant. You didn't really get an answer on if it is cursed.

Terminology note:

I think the examples would be easier to follow if we only used the word constructor to mean the procedure which fills out the fields of a structure instance and is bound by #:constructor-name.

By default the name of the struct is bound to the constructor, but the purpose of the mechanism I'm proposing is to bind the name to something that is not the constructor. In my original post I called it the name expression (#:name-expr).

Thanks for the examples. I think I'm coming back up to speed. Now I see how the structure name expands to the value binding.

For anyone lurking and confused, note that a value can act as both a structure and a procedure in Racket, and take a look at the explanation in Flatt/Culpepper/and friends Macros that work together, which I quote from below:

(define-struct egg (color size))

"The implementation of match uses syntax-local-value on the egg identifier to learn about its expected number of fields, its predicate, and its selector functions."

"Using define-syntax for both macros and other compile-time bindings allows a single identifier to play multiple roles. For example, make-struct-desc in the expansion of Racket’s define-struct macro produces a value that is both a structure description and a function. Since the descriptor is also a function, it can act as a macro transformer when egg is used as an expression. The function behavior of a structure descriptor is to return the identifier of structure’s constructor..."

The actual struct macro is complicated and uses lots of helpers, even looking at the expansions with the macro stepper takes a bit of digging to understand.