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

**URL:** <https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571>\
**Category:** Questions & Answers\
**Created:** [February 16, 2025, 9:25pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571 "2025-02-16T21:25:14Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![bakgatviooldoos](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/bakgatviooldoos/32/1381_2.png) [@bakgatviooldoos](https://racket.discourse.group/u/bakgatviooldoos)\
**Post date:** [February 16, 2025, 9:25pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/1 "2025-02-16T21:25:14Z")

</div>

Hi, Racket Discourse.

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

```scheme
;; 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

```

```scheme
; 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.

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [February 17, 2025, 2:05pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/2 "2025-02-17T14:05:26Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![bakgatviooldoos](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/bakgatviooldoos/32/1381_2.png) [@bakgatviooldoos](https://racket.discourse.group/u/bakgatviooldoos)\
**Post date:** [February 17, 2025, 4:39pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/3 "2025-02-17T16:39:20Z")

</div>

> [@greghendershott](#):
>
> the source code might have a "TO-DO"

I remembered seeing [something similar](https://racket.discourse.group/t/using-match-ellipses-on-multiple-elements/2891) recently posted by @jbclements about `~seq`-like behaviour in `match`, which might be related to why this doesn't work, in general?

> [@greghendershott](#):
>
> I'm surprised that it prints a message to stderr

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.

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [February 17, 2025, 4:59pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/4 "2025-02-17T16:59:54Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [February 17, 2025, 5:03pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/5 "2025-02-17T17:03:17Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![bakgatviooldoos](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/bakgatviooldoos/32/1381_2.png) [@bakgatviooldoos](https://racket.discourse.group/u/bakgatviooldoos)\
**Post date:** [February 18, 2025, 9:56am UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/6 "2025-02-18T09:56:35Z")

</div>

> [@greghendershott](#):
>
> how it is awkward to detect/handle the error at compile time, for some reason

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.

```scheme
#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 ○]))

```

```scheme
; 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:

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

```

---

<div class="post-metadata">

**Author:** ![samth](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/samth/32/3_2.png) [@samth](https://racket.discourse.group/u/samth)\
**Post date:** [February 18, 2025, 2:44pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/7 "2025-02-18T14:44:05Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![bakgatviooldoos](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/bakgatviooldoos/32/1381_2.png) [@bakgatviooldoos](https://racket.discourse.group/u/bakgatviooldoos)\
**Post date:** [February 18, 2025, 4:27pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/8 "2025-02-18T16:27:23Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![bakgatviooldoos](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/bakgatviooldoos/32/1381_2.png) [@bakgatviooldoos](https://racket.discourse.group/u/bakgatviooldoos)\
**Post date:** [February 23, 2025, 6:31pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/9 "2025-02-23T18:31:08Z")

</div>

Replying to this for posterity, but this is an odd case where `,@` "breaks", too:

```scheme
(require racket/match)
(let ([y '(3 4 5 6 7)])
  (match '(1 2 3 4 5 6 7)
    [`(,x ..2 ,@`,(and (== y) z)) `(,x ,@z)]))
;=> '((1 2) 3 4 5 6 7)

(let ([y '(3 4 5 6)])
  (match '(1 2 3 4 5 6 7)
    [`(,x ..2 ,@`,(and (== y) z) 7) `(,x ,@z)]))
;=> match: non-list pattern inside unquote-splicing in: (quasiquote (unquote (and (== y) z)))

```

If this weird pattern is in the final position in its container list, all is well. If anything appears after it, however, it fails.

---

<div class="post-metadata">

**Author:** ![greghendershott](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/greghendershott/32/98_2.png) [@greghendershott](https://racket.discourse.group/u/greghendershott)\
**Post date:** [February 27, 2025, 5:58pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/10 "2025-02-27T17:58:14Z")

</div>

A related thought:

AFAICT Racket doesn't really have an official/polished API and UX for non-exn "warnings", as opposed to "errors".

Various things use `log-warning` or `log-error` with various logger topics.

This works OK-ish when people see the logger output. But people can miss it.

Also it's just text -- but would be nice if those syntax objects were available "live" for their srcloc, so a tool like DrRacket or Racket Mode could jump there.

* * *

Maybe there could be something roughly like the mechanism that lets Typed Racket relay, during expansion, `mouse-over-tooltip` syntax properties to an `online-check-syntax` logger. With some easy front end like `error` (but called say `warning`).

I think this could work for both expansion and runtime warnings?

* * *

So few things in Racket issue warnings, that maybe this isn't needed? OTOH maybe more would, if it were available? I honestly don't know which.

---

<div class="post-metadata">

**Author:** ![samth](https://yyz2.discourse-cdn.com/free1/user_avatar/racket.discourse.group/samth/32/3_2.png) [@samth](https://racket.discourse.group/u/samth)\
**Post date:** [February 27, 2025, 7:19pm UTC](https://racket.discourse.group/t/behavioural-differences-in-match-for-splicing-single-pattern-variables/3571/11 "2025-02-27T19:19:58Z")

</div>

> [@greghendershott](#):
>
> Maybe there could be something roughly like the mechanism that lets Typed Racket relay, during expansion, `mouse-over-tooltip` syntax properties to an `online-check-syntax` logger. With some easy front end like `error` (but called say `warning`).
> 
> I think this could work for both expansion and runtime warnings?

I think a mechanism like this would be great. I have often wanted to be able to issue warnings from Typed Racket as well, and having that be available from other front ends would be really important.
