How to access syntax-class attributes in `~optional` that contains ellipsis?
Asked Answered
P

2

11

TL;DR

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.

Complete runnable example

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" '()))

)
Privation answered 7/3, 2019 at 13:54 Comment(0)
M
11

There are two classes of strategies to fix "attribute value is false" errors:

  1. 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

  2. Deal with the attribute being false in the body.

    (a) Using unsyntax and if

    (b) Using ~?

1 (a)

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

1 (b)

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

1 (c)

(define-splicing-syntax-class maybe-objects
  #:datum-literals (objects)
  [pattern (objects <object>:obj-exp ...)]
  [pattern (~seq) #:with (<object>:obj-exp ...) null])

2 (a) and (b)

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)))]))
Mather answered 7/3, 2019 at 14:54 Comment(1)
Awesome! Now I know a lot of different ways to tackle some syntax-parse issues, using ~? and ~parse, which I didn't knew about before. Thanks :)Informed
P
4

When using #:defaults, you need to specify the attribute:

(~optional <object>:obj-exp ... #:defaults ([(<object>.result 1) null]))

Complete code:

(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

Privation answered 7/3, 2019 at 13:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.