Zoé Martin
Zoé Martin

Reputation: 1907

How to override a struct constructor while still providing the struct metadata for compile time in Racket?

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"]))

Failing: using a match expander

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)

Question

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

Answers (1)

Zoé Martin
Zoé Martin

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.

Note

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

Related Questions