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

2

6

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?

Cloak answered 3/7, 2015 at 18:2 Comment(0)
C
3

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.

Continuation answered 3/7, 2015 at 18:41 Comment(6)
Ah, I didn't realize you could specify a specific constructor with #:constructor-name. That's very helpful.Cloak
I had a few problems using this macro? I can't squeeze it into a comment so I made another answer.Damocles
The simple wrapper example you gave doesn't seem to work. As Greg points out below, you can't redefine the function struct-id.Cloak
On a different note, in this case, I do want to use the dot notation because the examples I'm thinking of aren't just taking a few optional arguments, but arbitrarily many, which will be bundled up into a list. I'd just like to be able to call the constructor without having to use list notation on them (e.g., (queue item1 item2 ...) rather than (queue (list item1 item2 ...)).Cloak
I fixed the duplicate-definition problem.Continuation
You'll need to have some form of keyword arguments if there are arbitrarily many optional arguments and a random selection of them will be used. Although the next problem you'll run into is that Racket's apply function is completely broken when it comes to keywords.Continuation
D
3

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.

Damocles answered 4/7, 2015 at 1:3 Comment(5)
I'll probably just end up using the alternative constructor name as you suggest. It's what I was going to do anyway, but I was hoping that it was possible to make the original constructor work with arbitrarily many arguments.Cloak
I see. I never tried using this code from a module. It works fine at the REPL, where you're allowed to redefine things.Continuation
The loop generates (kw (id default)) not (kw id), and it is flattened by the use of two collect clauses in the loop macro, one for the keyword, and the other for the (id default-value) pair.Continuation
Also, in my opinion Racket's new pkgs system is markedly inferior to the old PLaneT system. That is why I am still using PLaneT.Continuation
@ErikWennstrom Although I haven't tried it, you could probably use (provide (rename-out [original alternative])) to get that effect outside the module, at least.Damocles

© 2022 - 2024 — McMap. All rights reserved.