I'm trying to expand an optional ellipsis (~optional datum:my-class ...)
into a form that uses attributes from my-class
like so: #'(list datum.attr ...)
.
But as the form is optional, when it's not present, it resolves to #f
, which the ellipsis pattern doesn't like:
;; ?: attribute contains non-list value
;; value: #f
I tried using the #:defaults
argument of ~optional
, like so:
(~optional datum:my-class ... #:defaults ([(datum 1) null]))
;; which works for usage:
#'(list datum ...)
;; but not for:
#'(list datum.attr ...)
I got it to work by using this trick:
(~optional datum:my-class ...)
#'(list #,@(if (attribute datum) #'(datum.attr ...) #'()))
I'm trying to find if there's a better way.
I tried keeping it to the bare minimum. Check out the test submodule.
To see the issue, uncomment one of the four implementations of parse-bag
, and run raco test file.rkt
.
#lang racket/base
(provide parse-bag)
(require
(for-syntax racket/base syntax/parse)
syntax/parse)
(struct bag (label objects) #:transparent)
(struct object (name) #:transparent)
(begin-for-syntax
(define-syntax-class obj-exp
#:datum-literals (object name)
(pattern (object (name <name>:str))
#:with result #'(object <name>))))
;; IMPLEMENTATION ONE
;; DESC: The naive but failing approach
;; UNCOMMENT TO TEST
#;(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)))
#'(bag <label>
(list <object>.result ...))]))
;; ?: attribute contains non-list value
;; value: #f
;; IMPLEMENTATION TWO
;; DESC: adding defaults will fail too
;; UNCOMMENT TO TEST
#;(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)
#:defaults ([(<object> 1) null]))) ; (HERE)
#'(bag <label>
(list <object>.result ...))]))
;; ?: attribute contains non-list value
;; value: #f
;; IMPLEMENTATION THREE
;; DESC: it won't fail when not using syntax-class attributes
;; UNCOMMENT TO TEST
#;(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)
#:defaults ([(<object> 1) null])))
#'(bag <label>
(list <object> ...))])) ; (HERE)
;; name: unbound identifier
;; IMPLEMENTATION FOUR
;; DESC: it works, but I find it ugly
;; UNCOMMENT TO TEST
#;(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)))
#`(bag <label>
(list #,@(if (attribute <object>)
#'(<object>.result ...)
#'())))]))
(module+ test (require rackunit)
(check-equal?
(parse-bag
(label "Biscuits")
(objects (object (name "Cookie"))
(object (name "Brownie"))))
(bag "Biscuits"
(list (object "Cookie")
(object "Brownie"))))
(check-equal?
(parse-bag (label "Sweets"))
(bag "Sweets" '()))
)
There are two classes of strategies to fix "attribute value is false" errors:
Put defaults or alternatives so that the attribute is never false.
(a) Using ~optional
with #:defaults
(b) Using ~or
, possibly with ~parse
(c) Using a syntax-class with multiple patterns
Deal with the attribute being false in the body.
(a) Using unsyntax
and if
(b) Using ~?
Implementations 2 and 3 in the question, as well as the code in Zoé's answer, all use ~optional
with #:defaults
, so they all are attempts to fix it with 1 (a).
Zoé's answer shows a correct use of this. The main point is that if you use an attribute like <object>.result
, you need to specify a default for <object>.result
, not just <object>
.
However, one downside to this comes if there are multiple attributes in the obj-exp
class that you need to use. That would require you to specify a default for each one:
(~optional (objects <object>:obj-exp ...)
#:defaults ([(<object>.result 1) null]
[(<object>.x 1) null]))
If you use multiple attributes from a syntax class, such as <object>.result
, <object>.x
, <object>.y
, <object>.z
, etc, then 1 (a) would require you to specify a default for each one separately. To avoid that, instead of writing this:
(~optional (objects <object>:obj-exp ...)
#:defaults ([(<object>.result 1) null]
[(<object>.x 1) null]
[(<object>.y 1) null]
[(<object>.z 1) null]))
You can use ~or
and ~parse
like this:
(~or (objects <object>:obj-exp ...)
(~and (~seq) (~parse (<object>:obj-exp ...) null)))
(define-splicing-syntax-class maybe-objects
#:datum-literals (objects)
[pattern (objects <object>:obj-exp ...)]
[pattern (~seq) #:with (<object>:obj-exp ...) null])
Implementation 4 in the question uses unsyntax-splicing
and if
, so it's an example of 2 (a):
#,@(if (attribute <object>)
#'(<object>.result ...)
#'())
However, as you noted, this looks kind of ugly. And it also has another problem. If this itself were under an ellipsis, this breaks down because ellipses don't carry their effects inside a #,
or #,@
.
That's why ~?
exists for 2 (b). Instead of using #,@(if ....)
, you can use:
(~? (<object>.result ...) ())
That doesn't quite work, but this variant of it does:
(~? (list <object>.result ...) (list))
Using that in a variation on implementation 4:
(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)))
#`(bag <label>
(~? (list <object>.result ...) (list)))]))
When using #:defaults
, you need to specify the attribute:
(~optional <object>:obj-exp ... #:defaults ([(<object>.result 1) null]))
(define-syntax (parse-bag stx)
(syntax-parse stx
#:datum-literals (label objects)
[(_ (label <label>:str)
(~optional (objects <object>:obj-exp ...)
#:defaults ([(<object>.result 1) null]))) ; + attribute here
#'(bag <label>
(list <object>.result ...))])) ; now it works!
Another way would be to move the ellipsis usage to the syntax-class, as in this question: A splicing syntax class that matches an optional pattern and binds attributes
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With