Behavioural differences in `match` for splicing/single pattern variables

Hi, Racket Discourse.

Quick question: Why do these instances of the pattern variable A handle "equality" of A across the pattern differently?

;; the circles are printed `exp` structs and the squares are printed `log` structs
;; ditto for the brackets, except the very outermost, which is just a quoted list

; this instance, with the "single" `A`, works as expected
(define arrange*.gather*
  (match-lambda
    [`(,(exp `(,A ,(log B)))
       ,(exp `(,A ,(log C))))
     
     `(,(exp `(,A ,(log `(,@B ,@C)))))]
    [_
     #false]))

(arrange*.gather*
 (el `(○ ,(○ □))
     `(○ ,(○ □))))
;=> '((○ [○ □ ○ □]))

(arrange*.gather*
 (el `(○ ,(○ □))
     `(□ ,(○ □))))
;=> #false
; this instance, with the "splice" `A`, works unexpectedly
(define arrange*.gather**
  (match-lambda
    [`(,(exp `(,@A ,(log B)))
       ,(exp `(,@A ,(log C))))
     
     `(,(exp `(,@A ,(log `(,@B ,@C)))))]
    [_
     #false]))

(arrange*.gather**
 (el `(○ ,(○ □))
     `(○ ,(○ □))))
;=> '((○ [○ □ ○ □]))

(arrange*.gather**
 (el `(○ ,(○ □))
     `(□ ,(○ □))))
;=> '((□ [○ □ ○ □]))

When using the splicing pattern, only the last occurrence of the pattern variable is actually bound in the context of the body, it would seem; and so no "equality" is being checked. Is this the intended behaviour?

I can imagine why it might be that way, in the sense of the variable becoming "shadowed" as the pattern is worked from left to right--I just found it odd that there is a difference at all between the two cases.


Edit: perhaps there is a difference, because the pattern is technically illegal? Booting up DrRacket this morning, the file opened with a console window reporting:

non-linear pattern used in match with ... at #<syntax:el.rkt:220:23 A> and #<syntax:el.rkt:220:13 A>
non-linear pattern used in match with ... at #<syntax:el.rkt:255:17 A> and #<syntax:el.rkt:254:17 A>

I didn't notice this before, but it is possible that I simply missed the warning.

I'm surprised that it prints a message to stderr -- and, does so at run time.

Instead I'd expect it to raise a syntax error exception -- at compile time (because this is about the match pattern).

[This feels like the source code might have a "TO-DO" comment about how it is awkward to detect/handle the error at compile time, for some reason, and the runtime message is a temporary thing??]


Probably more interesting is why `,@A a.k.a. A ... doesn't work.

I remembered seeing something similar recently posted by @jbclements about ~seq-like behaviour in match, which might be related to why this doesn't work, in general?

Right? I have only ever encountered these a handful of times--makes me feel like the code's broken the fourth wall, so to speak.

p.s. I was mistaken, I see the message printed at compile a.k.a. expansion time, e.g. if check-syntax is done.

So the time is fine. It's message instead of exception, that's surprising.

p.p.s. FWIW the message goes to log-error -- which writes to stderr under most default configurations but not necessarily.

1 Like

It wasn't the worst thing to try and paper over, but I can definitely see why it might be problematic. This works (by renaming and checking equality for the same pattern variables), although it can be improved still to eliminate unnecessary let declarations and so on.

I had to "separate" the cases of patterns in the head and in the bodies of the match-clauses, which at least was easy enough with parameterized syntax-classes and some conventions.

#lang racket/base

(require
  (only-in
   racket/list flatten)
  (only-in
   racket/match match-lambda)
  (only-in
   racket/set list->set set-count)
  (only-in
   racket/format ~a)
  (for-syntax
   syntax/stx
   racket/base
   syntax/parse
   racket/syntax
   (only-in
    racket/list remove-duplicates group-by)))

(struct mark [sum]
  #:transparent
  #:guard
  (lambda (sum _)
    (if (list? sum) (flatten sum) (list sum))))

(define (format-sum sum none some)
  (if (null? sum) none (format some (apply ~a #:separator " " sum))))

(define ((mark-writer-maker none some) self port mode)
  (define sum (mark-sum self))
  (fprintf port (format-sum (mark-sum self) none some)))

(struct exp mark []
  #:transparent
  #:property prop:custom-print-quotable 'always
  #:methods gen:custom-write
  [(define write-proc (mark-writer-maker "○" "(~a)"))])

(struct log mark []
  #:transparent
  #:property prop:custom-print-quotable 'always
  #:methods gen:custom-write
  [(define write-proc (mark-writer-maker "□" "[~a]"))])

(struct neg mark []
  #:transparent
  #:property prop:custom-print-quotable 'always
  #:methods gen:custom-write
  [(define write-proc (mark-writer-maker "◇" "⟨~a⟩"))])

(define (same? . xs)
  (= 1 (set-count (list->set xs))))

(begin-for-syntax
  (define (push var)
    (define mask (generate-temporary var))
    (syntax-property mask 'self var))
  
  (define-literal-set forms
    #:datum-literals (≡ ○ □ ◇)
    (unquote-splicing quasiquote unquote quote))
  
  (define form? (literal-set->predicate forms))
  (define-syntax-class
    name
    (pattern foo:id
      #:when (not (form? #'foo))))
  
  (define-syntax-class
    (sum posn)
    #:attributes
    ([ex 0]
     [ns 0]
     [ls 0])
    #:literal-sets (forms)
    #:local-conventions
    ([term (term posn)])

    (pattern ,@var:name
      #:when (eq? 'head posn)
      #:attr sp (push #'var)
      #:attr ex #',@sp
      #:attr ns #'(sp)
      #:attr ls #false)

    (pattern ,@var:name
      #:when (eq? 'body posn)
      #:attr ex #',@var
      #:attr ns #'(var)
      #:attr ls #false)
    
    (pattern var:name
      #:attr ex #',var
      #:attr ns #'(var)
      #:attr ls #false)
    
    (pattern (term ...)
      #:attr ex #'(term.ex ...)
      #:attr ns #'((~@ . term.ns) ...)
      #:attr ls #true))
  
  (define-syntax-class
    (exp posn)
    #:attributes
    ([ex 0]
     [ns 0])
    #:literal-sets (forms)
    #:local-conventions
    ([sum (sum posn)])

    (pattern ○
      #:attr ex #',(exp null)
      #:attr ns #'())

    (pattern `sum
      #:attr ex (if (attribute sum.ls) #',(exp `sum.ex) #',(exp sum))
      #:attr ns #'sum.ns))
  
  (define-syntax-class
    (log posn)
    #:attributes
    ([ex 0]
     [ns 0])
    #:literal-sets (forms)
    #:local-conventions
    ([sum (sum posn)])

    (pattern □
      #:attr ex #',(log null)
      #:attr ns #'())

    (pattern ,sum
      #:attr ex (if (attribute sum.ls) #',(log `sum.ex) #',(log sum))
      #:attr ns #'sum.ns))

  (define-syntax-class
    (neg posn)
    #:attributes
    ([ex 0]
     [ns 0])
    #:literal-sets (forms)
    #:local-conventions
    ([sum (sum posn)])

    (pattern ◇
      #:attr ex #',(neg null)
      #:attr ns #'())

    (pattern 'sum
      #:attr ex (if (attribute sum.ls) #',(neg `sum.ex) #',(neg sum))
      #:attr ns #'sum.ns))

  (define-syntax-class
    (term posn)
    #:attributes
    ([ex 0]
     [ns 0])
    #:local-conventions
    ([sum (sum posn)]
     [exp (exp posn)]
     [log (log posn)]
     [neg (neg posn)])

    (pattern ex:nat
      #:attr ns #'())
    
    (pattern (~or* sum exp log neg)
      #:attr ex #'(~? sum.ex
                  (~? exp.ex
                  (~? log.ex
                      neg.ex)))

      #:attr ns #'(~? sum.ns
                  (~? exp.ns
                  (~? log.ns
                      neg.ns)))))

  (define-splicing-syntax-class
    (terms posn)
    #:attributes
    ([ex 0]
     [ns 0])
    #:local-conventions
    ([sum  (sum  posn)]
     [term (term posn)])
    
    (pattern sum
      #:attr ex (if (attribute sum.ls) #'`sum.ex #'sum)
      #:attr ns #'sum.ns)
    
    (pattern {~seq term ...}
      #:attr ex #'`(term.ex ...)
      #:attr ns #'((~@ . term.ns) ...)))

  (define-syntax-class
    clause
    #:attributes
    ([ex 0]
     [as 0])
    #:literal-sets (forms)
    
    (pattern [as:name foo:expr ...]
      #:attr ex #'(foo ...)))

  (define (derive-cases clauses)
    (define cls (stx->list clauses))
    (for/list ([bd (in-list cls)])
      (map (lambda (cl) `(,cl ,bd)) (remove bd cls))))

  (define (var-name x)
    (syntax-property x 'self))
  
  (define (masked-vars names)
    (filter var-name (stx->list names)))
  
  (define (group-names names)
    (define grouped (group-by var-name (masked-vars names) free-identifier=?))
    (define samemap (map (lambda (g) (if (null? (cdr g)) (car g) #`(same? . #,g))) grouped))
    (define rootmap (map (lambda (g) `(,(var-name (car g)) ,(car g))) grouped))
    `(,samemap ,rootmap))
  
  (define (map-groups names**)
    (map (lambda (names*) (map group-names (stx->list names*))) (stx->list names**)))

  (define-splicing-syntax-class
    system
    #:attributes
    ([ex 0]
     [ns 0]
     [as 0])
    #:local-conventions
    ([hd (terms 'head)]
     [bd (terms 'body)])

    (pattern {~seq lhs:clause rhs:clause ...+}
      #:with (({(hd) (bd)} ...) ...) (derive-cases #'(lhs.ex rhs.ex ...))
      #:with (({cd lt} ...) ...)     (map-groups #'((hd.ns ...) ...))
      
      #:attr ex
      #'(([hd.ex
           #:when (and . cd)
           (let lt bd.ex)]
          ...
          [_ #false])
         ...)
      #:attr ns #'((~@ . bd.ns) ... ...)
      #:attr as #'(lhs.as rhs.as ...)))

  (define (unique-names names)
    (remove-duplicates (stx->list names) free-identifier=?))

  (define ((format-rule stx ax) as)
    (format-id stx "~a.~a" ax as #:subs? #true))

  (define (format-rules stx ax as)
    (map (format-rule stx ax) (stx->list as))))

(define-syntax (define-axiom stx)
  (syntax-parse stx
    [(_ name:id :system)
     #:with (vars ...) (unique-names #'ns)
     #:with (rule ...) (format-rules stx #'name #'as)
     #:with (body ...) #'ex
     #'(define-values (rule ...)
         (let ([vars null] ...)
           (values (match-lambda . body) ...)))]))

(define-syntax (el stx)
  (syntax-parse stx
    #:local-conventions
    ([terms (terms 'body)])
    
    [(_ terms) #'terms.ex]))

(define-axiom arrange*
  [gather
   `(,@A ,(,@B ,@C ,@D))]
  
  [spread
   `(,@A ,B) `(,@A ,C) `(,@A ,D)])

(arrange*.gather
 (el `(○ ○ ○ ,(3 ○))
     `(○ ○ ○ ,(○ 4))
     `(○ ○ ○ ,(○))))

;=> '((○ ○ ○ [3 ○ ○ 4 ○]))
; the macro above becomes
(define-values
  (arrange*.gather arrange*.spread)
  (let ((A null) (B null) (C null) (D null))
    (values
     (match-lambda
       (`(,(exp `(,@A5 ,(log B))) ,(exp `(,@A6 ,(log C))) ,(exp `(,@A7 ,(log D))))
        #:when
        (and (same? A5 A6 A7))
        (let ((A A5)) `(,(exp `(,@A ,(log `(,@B ,@C ,@D)))))))
       (_ #f))
     (match-lambda
       (`(,(exp `(,@A8 ,(log `(,@B9 ,@C10 ,@D11)))))
        #:when
        (and A8 B9 C10 D11)
        (let ((A A8) (B B9) (C C10) (D D11)) `(,(exp `(,@A ,(log B))) ,(exp `(,@A ,(log C))) ,(exp `(,@A ,(log D))))))
       (_ #f)))))

The use of the let over the values is to ensure that asymmetric patterns (where a pattern variable occurs only in one or some but not all of the cases of the "axiom") remain valid, as in:

(define-axiom reflect
  [create A 'A] [cancel ()])

There's a warning because (a) this doesn't work correctly but (b) it was accidentally widely allowed and thus making it a syntax error would break working code.

It's certainly possible that this is the wrong choice of how to handle the situation, though, and I should consider whether to just make this an error.

That makes sense, thank you, @samth.

I guess leaving it as a warning isn't really terrible; I was surprised, but if there had been something in the docs alluding to or explaining some of the oddities regarding ,@ it might go some way to alleviate that kind of situation.

I searched through the reference and the guide for match quickly, but the artefact is only covered cursorily in the grammar section, and no place else I could readily see.