Search code examples
schemerefactoring

Refactoring Specialized Behavior from Common Code in Scheme


This is a problem I come up against all the time. I have a function that does something useful, but I want a version that does something slightly different as well.

For example, a substring search that finds all of the positions where some substring occurs in another, longer piece of text.

Then I discover a use case that only requires finding the first instance of the substring, or the last, or the n'th. Is there an idiomatic way to factor out the different behaviors from the larger body of common code?

For example, here are two substring search functions. One returns the first match. The other returns all matches. (Please ignore the fact that this is a poor way to do a substring search.)

;; Return the index of the first matching instance of `pattern` in
;; `s`. Returns #f if there is no match.
(define (naive-string-find-first pattern s)
  (let* ((p-len (string-length pattern))
         (limit (- (string-length s) p-len)))
    (let outer ((i 0))
      (if (<= i limit)
          (let inner ((j i)
                      (k 0))
            (if (< k p-len)
                (if (char=? (string-ref s j) (string-ref pattern k))
                    (inner (+ j 1) (+ k 1))
                    (outer (+ i 1)))
                i))
          #f))))

;; Return a list of all positions in `s` where `pattern` occurs.
;; Returns '() if there is no match.
(define (naive-string-find-all pattern s)
  (let* ((p-len (string-length pattern))
         (limit (- (string-length s) p-len)))
    (let outer ((i 0))
      (if (<= i limit)
          (let inner ((j i)
                      (k 0))
            (if (< k p-len)
                (if (char=? (string-ref s j) (string-ref pattern k))
                    (inner (+ j 1) (+ k 1))
                    (outer (+ i 1)))
                (cons i (outer (+ i 1)))))
          '()))))

As you can see, they are almost identical, differing only in the last two lines. Specifically, one of those lines handles proceeding from a match. The other handles proceeding from a failure to match anything.

What I would like to be able to do is something like:

(define (naive-string-find-common pattern s match-func fail-func)
  (let* ((p-len (string-length pattern))
         (limit (- (string-length s) p-len)))
    (let outer ((i 0))
      (if (<= i limit)
          (let inner ((j i)
                      (k 0))
            (if (< k p-len)
                (if (char=? (string-ref s j) (string-ref pattern k))
                    (inner (+ j 1) (+ k 1))
                    (outer (+ i 1)))
                (match-func i)))
          (fail-func i)))))

(define (naive-string-find-first-common pattern s)
  (let ((match-f (lambda (x) x))
        (fail-f (lambda (x) #f)))
    (naive-string-find-common pattern s match-f fail-f)))

(define (naive-string-find-all-common pattern s)
  (let ((match-f (lambda (x) (cons x (outer (+ x 1))))) ;; <-- Fails, of course.
        (fail-f (lambda (x) #f)))
    (naive-string-find-common pattern s match-f fail-f)))

Handling of the "find-first" behavior works. The "find-all" version fails because the specialization procedure has no knowledge of the named let outer.

Is there an idiomatic way to factor out the needed functionality in cases like this?


Solution

  • As @soegaard says, wrapper functions that add a "flag" argument are idiomatic and clear.

    The "What I would like to do" code could be repaired, for example (in Racket to use check-expect:

    #lang r6rs
    (import (rnrs) (test-engine racket-tests))
    
    (define (naive-string-find-common pattern s match-func fail-func)
      (let* ((p-len (string-length pattern))
             (limit (- (string-length s) p-len)))
        (let outer ((i 0))
          (if (<= i limit)
              (let inner ((j i)
                          (k 0))
                (if (< k p-len)
                    (if (char=? (string-ref s j) (string-ref pattern k))
                        (inner (+ j 1) (+ k 1))
                        (outer (+ i 1)))
                    (match-func i (lambda () (outer (+ i 1))) )))
              (fail-func i)))))
    
    (define (naive-string-find-first-common pattern s)
      (let ((match-f (lambda (x k) x))
            (fail-f (lambda (x) #f)))
        (naive-string-find-common pattern s match-f fail-f)))
    
    (define (naive-string-find-all-common pattern s)
      (let ((match-f (lambda (x k) (cons x (k))))
            (fail-f (lambda (x) '())))
        (naive-string-find-common pattern s match-f fail-f)))
    
    (check-expect (naive-string-find-first-common "ab" "abab") 0  )
    (check-expect (naive-string-find-first-common "ab" "a-b-") #f )
    
    (check-expect (naive-string-find-all-common   "ab" "abab") '(0 2) )
    (check-expect (naive-string-find-all-common   "ab" "a-b-") '()    )
    
    (test)