Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I write a constructor for a Scheme (Racket) struct that takes a variable number of arguments?

I understand how to write a function that takes an arbitrary number of arguments using the dot notation. Example: (define (func-name . args) func-body).

And I understand how to use a constructor guard to preprocess the constructors arguments, allowing me to pass different types to the constructor. Example:

(struct struct-id (field-ids)
    #:guard (lambda (field-ids type-name) process-fields))

But that's as close as I can get. Can you write a guard that takes an arbitrary number of arguments? Or is there some other way to modify what a struct constructor does?

like image 634
Erik Wennstrom Avatar asked Jul 03 '15 18:07

Erik Wennstrom


2 Answers

Although you can specify another name for the constructor with #:constructor-name, in my experience that doesn't "free up" the struct identifier to be used as a function name:

(struct baz (a b c) #:transparent #:constructor-name -baz)
(-baz 1 2 3) ;(baz 1 2 3)
(define (baz a b c)
  (-baz 1 2 3))
; module: duplicate definition for identifier
;   at: baz
;   in: (define-values (baz) (new-lambda (a b c) (-baz 1 2 3)))

So I usually just define an alternative constructor using another name, and use that instead of the default one (which is still available to use, if desired).


As for a macro to do this, plus supply keyword args and optional args: I wasn't able to get the macro in the other answer to work for me.

  • The first problem is the one I mention above.
  • It isn't append*-ing (flattening) the (#:kw id) arg specs in the function definition.
  • It hardwires the identifier construct instead of one formed from the user's struct identifier.
  • Finally I prefer not to use a CL loop macro, pulled in from an old-style PLaneT package.

Instead, what does work for me is modifying and extending the code I showed in a blog post, as follows:

#lang racket/base

(require (for-syntax racket/base
                     racket/list
                     racket/syntax
                     syntax/parse))

(begin-for-syntax
 (define syntax->keyword (compose1 string->keyword symbol->string syntax->datum)))

(define-syntax (struct/kw stx)
  (define-syntax-class field
    (pattern id:id
             #:with ctor-arg #`(#,(syntax->keyword #'id) id))
    (pattern [id:id default:expr]
             #:with ctor-arg #`(#,(syntax->keyword #'id) [id default])))
  (syntax-parse stx
    [(_ struct-id:id (field:field ...) opt ...)
     (with-syntax ([ctor-id (format-id #'struct-id "~a/kw" #'struct-id)]
                   [((ctor-arg ...) ...) #'(field.ctor-arg ...)]) ;i.e. append*
       #'(begin
           (struct struct-id (field.id ...) opt ...)
           (define (ctor-id ctor-arg ... ...) ;i.e. append*
             (struct-id field.id ...))))]))

Example usage:

;; Define a struct type
(struct/kw foo (a b [c #f]) #:transparent)

;; Use normal ctor
(foo 1 2 3)                ; => (foo 1 2 3)

;; Use keyword ctor
(foo/kw #:a 1 #:b 2 #:c 3) ; => (foo 1 2 3)

;; Use keyword ctor, taking advantage of default arg for #:c field
(foo/kw #:a 1 #:b 2)       ; => (foo 1 2 #f)

Granted this is simplistic, it would need more work to support everything normal struct can do.

like image 143
Greg Hendershott Avatar answered Nov 06 '22 16:11

Greg Hendershott


Just write a wrapper:

(struct struct-id (a b c d) #:constructor-name struct-id*
      #:guard (lambda (a b c d type-name) do-stuff))

(define (struct-id (a) (b) (c) (d 'default-value))
        (struct-id* a b c d))

That gives you a constructor in which all the field arguments are optional. Defining them this way instead of with dot notation saves you from having to parse through the rest-argument.

I supplied a default value for d, and Racket will make the default value of the others #f.

You can also define it to have keyword arguments:

(define (struct-id #:a (a #f) #:b (b #f) #:c c #:d (d 'default))
        (struct-id* a b c d))

In the above case, #:c is a required argument because I left off the parentheses, I provided 'default as the default value of d, and the others will have a default value of #f, which this time has to be explicitly provided. Keywords can be passed to the constructor in any order.

If you're using a lot of structs, you might want a macro to define the wrapper for you:

(begin-for-syntax
 (require (planet jphelps/loop)) ;; Installs a library on first use. Be patient.
 (define stx-symbol->string (compose symbol->string syntax->datum))
 (define (make-constructor-name stx-name)
    (datum->syntax stx-name
      (string->symbol
       (string-append (stx-symbol->string stx-name) "*"))))
 (define (stx-symbol->stx-keyword stx-symbol)
   (datum->syntax stx-symbol
    (string->keyword
     (symbol->string
      (syntax->datum stx-symbol))))))

(define-syntax struct*
  (lambda (stx)
    (syntax-case stx ()
        ((_ struct-name fields . options)
         #`(begin
             (struct struct-name fields . options)
             (define (#,(make-constructor-name #'struct-name) 
                . #,(loop for name in (syntax-e #'fields)
                        collect (stx-symbol->stx-keyword name)
                        collect #`(#,name #f)))
               (struct-name . fields)))))))

Then define your structs like this:

(struct* struct-id (a b c d) #:guard whatever)

You'll automatically get a keyword-based constructor named struct-id* that doesn't conflict with names that are generated by the struct form.

EDIT

Apparently the above macro as it was originally written didn't work in module-based programs. I only tested it at the REPL, which behaves more like a Lisp in that you're allowed to redefine things. This masked the fact that struct's #:constructor-name option adds and additional constructor name instead of overriding the existing constructor name. This is in spite of the fact that there's an #:extra-constructor-name option that also creates an additional constructor name.

Fixing this problem in a way that would be completely seamless would require you to reimplement the entire struct macro. You'd have to rename the struct and then generate not only the constructor, but all of the accessors and mutators. An easier workaround would be to generate a constructor with a different name from the original constructor. I have edited the code above to implement this workaround.

like image 24
Throw Away Account Avatar answered Nov 06 '22 16:11

Throw Away Account