Reputation: 1907
This might seems similar to questions like Overloading a struct constructor? or Overloading a struct constructor. But none of those question tackle the issue of passing the overloaded identifier out the module boundaries (by providing it).
For example, let's say I have a struct I want to overload the constructor:
(struct fish (weight scales))
(define (make-fish [weight 5] [scales 'blue])
(fish weight scales))
Now I want to provide the new contructor so that it has the name of the struct, to make its usage completely transparent:
(provide
(except-out (struct-out fish) fish)
(rename-out (make-fish fish)))
This will work most of the time. But there are small subtle bugs that can arise.
Inheriting the struct is not possible any more, nor using match
:
(require animals/fish)
(struct shark fish (teeth)) ;; ERROR: parent struct type not defined
(define (describe-animal animal)
(match animal
[(fish weight scales) ;; ERROR: syntax error in pattern
(format "A ~a pounds fish with ~a scales" weight scales)]
[_ "Not a fish"]))
Creating the match expander (accepted solution in linked questions).
It won't work because you can't export the match-expander as a struct.
#lang racket/base
(require
(for-syntax
racket/base
syntax/transformer)
racket/match)
(provide
(except-out (struct-out fish) fish)
(rename-out (make-fish fish)))
(struct fish (weight scales)
#:name private-fish
#:constructor-name private-fish)
(define (make-fish [weight 5] [scales 'blue])
(private-fish weight scales))
(define-match-expander fish
(lambda (stx)
(syntax-case stx ()
[(_ field ...) #'(private-fish field ...)]))
(make-variable-like-transformer #'private-fish))
You get the error:
struct-out: identifier is not bound to struct type information
at: fish
in: (struct-out fish)
So how do we change the constructor of a struct, but still allow it to be provided and used as a parent in other structs?
Upvotes: 4
Views: 196
Reputation: 1907
Using a metadata struct, which is just a struct defined at compile time, you can encapsulate a struct definition at compile time into a value that can be used for match
and for inheriting.
#lang racket/base
(require
(for-syntax
racket/base
racket/struct-info
syntax/transformer)
racket/match)
(provide
(struct-out fish))
(struct fish (weight scales)
#:name private-fish
#:constructor-name private-fish)
(define (make-fish [weight 5] [scales 'blue])
(private-fish weight scales))
(begin-for-syntax
;; we define a struct that will only exist at compile time
;; and can encapsulate an identifier
(struct metadata (ctor struct-info)
#:property prop:procedure (struct-field-index ctor)
#:property prop:struct-info (lambda (self) (metadata-struct-info self))))
(define-syntax fish ;; this variable can be used like the initial struct when compiling
(metadata
(set!-transformer-procedure
(make-variable-like-transformer #'make-fish))
(extract-struct-info (syntax-local-value #'private-fish))))
This struct must have specific properties: prop:procedure
, so that it still works as a constructor, and prop:struct-info
, so that match
and struct
can fetch struct informations at compile time.
Please take note that in the next release of Racket, thanks to a PR by Alex Knauth, set!-transformer-procedure
will not be needed any more, and you'll just have to call make-variable-like-transformer
.
Upvotes: 4