Search code examples
pattern-matchingmatchracketsublist

How to bind an identifier to a sublist plus delimiter?


This match binds identifiers a and b to the prefix '(0 1) and the suffix '(3 4 5) of the list:

(match '(0 1 2 3 4 5)
  [`(,a ... 2 ,b ...)
   (values a b)])

Another equivalent version:

(match '(0 1 2 3 4 5)
  [`(,@(list a ... 2) ,b ...)
   (values a b)])

How to bind an identifier (within the pattern itself) to the prefix '(0 1 2), including the delimiter?


Solution

  • The app pattern, which invokes a function with the value being matched and then matches the values it returns, combined with a version of splitf-at that includes the partition element in the first list instead of the second, can be used to do this:

    ; Like splitf-at but includes the element to split at in the first value not the second
    (define (splitf-at/inclusive lst pred?)
      (let loop ([lst lst]
                 [first-res '()])
        (cond
          ((empty? lst)
           (values (reverse first-res) '()))
          ((pred? (car lst))
           (loop (cdr lst) (cons (car lst) first-res)))
          (else
           (values (reverse (cons (car lst) first-res)) (cdr lst))))))
    
    ; Gives '(0 1 2) '(3 4 5)
    (match '(0 1 2 3 4 5)
      ((? list? (app (lambda (lst) (splitf-at/inclusive lst (negate (curry = 2)))) a b))
       (values a b)))
    

    (Note the use of (? list? ...) to make sure the value is a list before trying to call any functions that depend on that.)

    You can define a match extender to make it nicer-looking:

    (define-match-expander split-list
      (lambda (stx)
        (syntax-case stx (...)
          ((split-list x (... ...) val y (... ...))
           #'(? (lambda (lst) (and (list? lst) (member val lst)))
                (app (lambda (lst) (splitf-at/inclusive lst (lambda (elem) (not (equal? elem val))))) x y))))))
    
    ; Also gives '(0 1 2) '(3 4 5)
    (match '(0 1 2 3 4 5)
      ((split-list a ... 2 b ...)
       (values a b)))
    

    This version also includes a check to make sure the value you want to split on is actually in the list, or it'll fail to match.