超越 Lisp 中传统的模式匹配
Beyond Traditional Pattern Matching in Lisp

原始链接: https://github.com/naver/lispe/wiki/6.1-Pattern-Matching-in-LispE

LispE是Naver开发的一种Lisp方言,其显著特点在于`defpat`、`defmacro`和`defpred`,分别提供了高级模式匹配、宏功能和逻辑编程元素。 `defpat`允许基于参数模式进行函数多态,包括用于序列匹配的Kleene运算符,这简化了与Common Lisp、Scheme和Clojure的手动条件语句相比的逻辑处理。 `defmacro`增强了宏的模式匹配功能并加入了`$`运算符,通过减少其他Lisp语言中需要的手动解构,从而简化了自定义语法创建。 `defpred`集成了谓词逻辑和回溯机制,将每条指令都视为布尔测试。失败将触发回溯到备选定义,从而提供了其他Lisp语言中所没有的声明式逻辑编程能力。 这些特性使得LispE在表达性和模块化方面表现出色,尤其是在处理复杂数据、自定义语法和逻辑推理方面,它为Lisp的发展提供了独特的视角。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Lisp 中超越传统模式匹配 (github.com/naver) 7 分,作者 PaulHoule,2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 加入我们 6 月 16-17 日在旧金山举办的 AI 初创公司学校! 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

LispE, a modern Lisp dialect developed by Naver, introduces innovative constructs—defpat, defmacro, and defpred—that set it apart from traditional Lisp implementations like Common Lisp, Scheme, and Clojure. While Lisp dialects share a heritage of flexibility and macro systems, LispE extends this with advanced pattern matching, enhanced macro capabilities, and logic programming elements. This blog entry examines these features, comparing them to their counterparts in other Lisps with specific examples to highlight LispE’s unique approach.

defpat: Pattern Matching in Function Definitions

LispE’s defpat enables defining multiple functions under the same name, each triggered by a specific argument pattern. This offers a declarative way to handle polymorphism beyond type-based dispatching.

LispE Example 1 (FizzBuzz):

(defun checkmod (v d) (eq (% v d) 0))
(defpat fizzbuzz ([integer_ (checkmod x 15)]) 'fizzbuzz)
(defpat fizzbuzz ([integer_ (checkmod x 3)]) 'fizz)
(defpat fizzbuzz ([integer_ (checkmod x 5)]) 'buzz)
(defpat fizzbuzz (x) x)
(mapcar 'fizzbuzz (range 1 15 1))

Output: (1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz). The nested patterns (e.g., (integer_ (checkmod x 3))) match integers divisible by 3, with a fallback for unmatched cases.

LispE Example 2 (Kleene Operator): To further illustrate defpat’s capabilities, consider its support for Kleene operators (+, *, %), which allow matching sequences of arguments.

  • a% : means that the element is optional
  • a* : means that this element can be present 0 or n times
  • a+ : means that this element should be present at least once

Here’s an example that processes a list of numbers, collecting those less than 10:

(defpat collect-small ([(< x 10)+ $ rest])
   (println "Collected:" x "Remaining:" rest)
   x
)
(defpat collect-small (lst)
   (println "No small numbers in:" lst)
   '()
)
(collect-small '(5 8 12 3 15))

Output:

Collected: (5 8) Remaining: (12 3 15)
(5 8)

Here, [(< x 10)+ $ rest] uses the + operator to match one or more numbers less than 10, binding them to x, while $ rest captures the remaining elements. If no numbers match the condition, the fallback definition applies. This demonstrates how defpat can concisely handle sequence patterns directly in the parameter list.

  • Common Lisp: Lacks native pattern matching. Using CLOS generic functions requires defining methods for types, but condition-based or sequence matching needs manual logic:

    (defun checkmod (v d) (= (mod v d) 0))
    (defgeneric fizzbuzz (x))
    (defmethod fizzbuzz ((x integer))
      (cond ((checkmod x 15) 'fizzbuzz)
            ((checkmod x 3) 'fizz)
            ((checkmod x 5) 'buzz)
            (t x)))
    (mapcar #'fizzbuzz (loop for i from 1 to 15 collect i))

    For the Kleene-like case:

    (defun collect-small (lst)
      (let ((small (loop for x in lst while (< x 10) collect x)))
        (if small
            (progn
              (format t "Collected: ~a Remaining: ~a~%" small (nthcdr (length small) lst))
              small)
            (progn
              (format t "No small numbers in: ~a~%" lst)
              '()))))
    (collect-small '(5 8 12 3 15))

    This requires explicit looping and list manipulation, contrasting with LispE’s concise pattern.

  • Scheme: Relies on explicit conditionals without pattern matching:

    (define (checkmod v d) (= (modulo v d) 0))
    (define (fizzbuzz x)
      (cond ((checkmod x 15) 'fizzbuzz)
            ((checkmod x 3) 'fizz)
            ((checkmod x 5) 'buzz)
            (else x)))
    (map fizzbuzz (iota 15 1))

    For the sequence example:

    (define (collect-small lst)
      (let loop ((in lst) (small '()))
        (cond ((null? in)
               (if (null? small)
                   (begin (display "No small numbers in: ")A (display lst) (newline) '())
                   (begin (display "Collected: ") (display (reverse small))
                          (display " Remaining: ") (display in) (newline)
                          (reverse small))))
              ((< (car in) 10)
               (loop (cdr in) (cons (car in) small)))
              (else
               (display "Collected: ") (display (reverse small))
               (display " Remaining: ") (display in) (newline)
               (reverse small)))))
    (collect-small '(5 8 12 3 15))

    The logic is centralized and recursive, lacking LispE’s declarative sequence matching.

  • Clojure: Multimethods offer dispatching, but complex patterns need custom logic:

    (defn checkmod [v d] (zero? (mod v d)))
    (defmulti fizzbuzz (fn [x] [(integer? x) x]))
    (defmethod fizzbuzz [true 15] [_] :fizzbuzz)
    (defmethod fizzbuzz [true 3] [_] :fizz)
    (defmethod fizzbuzz [true 5] [_] :buzz)
    (defmethod fizzbuzz :default [x] x)
    (map fizzbuzz (range 1 16))

    For the Kleene case:

    (defn collect-small [lst]
      (let [small (take-while #(< % 10) lst)
            rest (drop (count small) lst)]
        (if (seq small)
            (do (println "Collected:" small "Remaining:" rest) small)
            (do (println "No small numbers in:" lst) '()))))
    (collect-small [5 8 12 3 15])

    This uses functional utilities but lacks the pattern-based brevity of defpat.

LispE’s defpat stands out with its ability to embed conditions and Kleene operators directly in parameters, offering a more expressive and modular alternative to the manual logic required in other Lisps.

defmacro: Enhanced Macros with Pattern Matching

LispE’s defmacro extends the traditional Lisp macro system with pattern matching and a $ operator, simplifying the creation of custom syntax that adapts to argument structures.

LispE Example (Custom Loop):

(defmacro tang (('< x y) $ z)
   (loop x (range 0 y 1) $ z)
)
(defmacro tang (('> x y) $ z)
   (loop x (range y 0 -1) $ z)
)
(tang (< x 5) (println (* 2 x)))

Expands to (loop x (range 0 5 1) (println (* 2 x))), looping upward. The $ z captures and inserts additional arguments.

  • Common Lisp: Macros require manual destructuring:

    (defmacro tang (direction-and-vars &rest body)
      (let ((dir (car direction-and-vars))
            (x (cadr direction-and-vars))
            (y (caddr direction-and-vars)))
        (if (eq dir '<)
            `(loop for ,x from 0 below ,y do ,@body)
            `(loop for ,x from ,y downto 0 do ,@body))))
    (tang (< x 5) (format t "~d~%" (* 2 x)))

    This manually parses the direction and variables, requiring more code than LispE’s pattern-based approach.

  • Scheme: Hygienic macros limit flexibility, but a traditional macro works similarly:

    (define-syntax tang
      (lambda (stx)
        (syntax-case stx (< >)
          [(_ (< x y) body ...)
           #'(let loop ((x 0))
               (when (< x y)
                 body ...
                 (loop (+ x 1))))]
          [(_ (> x y) body ...)
           #'(let loop ((x y))
               (when (>= x 0)
                 body ...
                 (loop (- x 1))))])))
    (tang (< x 5) (display (* 2 x)) (newline))

    The syntax-case system requires explicit pattern handling, less streamlined than LispE’s $.

  • Clojure: Macros need manual argument processing:

    (defmacro tang [[op x y] & body]
      (if (= op '<)
        `(doseq [~x (range 0 ~y)] ~@body)
        `(doseq [~x (range ~y -1 -1)] ~@body)))
    (tang (< x 5) (println (* 2 x)))

    This parses the operator and constructs the loop, but lacks LispE’s pattern automation.

LispE’s defmacro simplifies macro writing with pattern matching and the $ operator, reducing the manual effort seen in other Lisps and enabling more modular syntax definitions.

Below is a rewritten defpred section for the blog entry, expanded to provide more detail on how each instruction in a defpred function is handled as a Boolean test that can fail execution, triggering backtracking. The rest of the blog remains unchanged, so I’ve only included the updated section here. The tone remains factual and descriptive, with added depth to clarify the mechanism.


defpred: Predicate Logic and Backtracking

LispE’s defpred introduces a unique approach to function definitions by combining pattern matching with predicate-based evaluation and automatic backtracking, integrating elements of logic programming into a Lisp framework. Unlike traditional functions, where instructions are executed sequentially for their side effects or return values, defpred treats each instruction in the function body as a Boolean test. If any test evaluates to false (represented as nil or 0 in LispE), the function fails, and LispE backtracks to try the next applicable definition. This mechanism allows defpred to explore alternative execution paths declaratively.

LispE Example (Filtering Numbers):

(defpred teste ([]) 
   true
)
(defpred teste ([a $ b])
   (< a 10)
   (println a)
   (teste b)
)
(defpred teste (l)
   (println "We stop" l)
)
(teste '(1 2 11 12 13))

Output: 1 2 We stop (11 12 13). This example filters numbers less than 10 from a list, printing them, and stops when a number fails the condition.

To understand how this works, consider the execution step-by-step:

For the input (1 2 11 12 13):

  • With a = 1, b = (2 11 12 13): (< 1 10) is true, (println 1) succeeds (prints 1), and (teste (2 11 12 13)) proceeds.
  • With a = 2, b = (11 12 13): (< 2 10) is true, (println 2) succeeds (prints 2), and (teste (11 12 13)) proceeds.
  • With a = 11, b = (12 13): (< 11 10) is false, causing the second definition to fail. LispE backtracks to the third definition, which matches (11 12 13) and executes (println "We stop" (11 12 13)), printing the message and succeeding.

This Boolean evaluation per instruction, coupled with backtracking, allows defpred to handle logic declaratively, stopping at the first fully successful definition.

  • Common Lisp: Requires explicit control flow, with no native backtracking or predicate evaluation:

    (defun teste (lst)
      (cond ((null lst) t)
            ((< (car lst) 10)
             (format t "~d~%" (car lst))
             (teste (cdr lst)))
            (t (format t "We stop ~a~%" lst))))
    (teste '(1 2 11 12 13))

    Each condition is checked manually, and execution continues without failure-based branching. The cond form doesn’t treat instructions as predicates that can fail and trigger alternatives; it’s a straightforward decision tree.

  • Scheme: Lacks built-in backtracking, relying on manual recursion:

    (define (teste lst)
      (if (null? lst)
          #t
          (if (< (car lst) 10)
              (begin (display (car lst)) (newline) (teste (cdr lst)))
              (begin (display "We stop ") (display (cdr lst)) (newline)))))
    (teste '(1 2 11 12 13))

    Instructions like (display ...) execute for their side effects, not as Boolean tests. Stopping at 11 is a single-pass decision, not a failure-driven backtrack. Continuations could simulate this, but they require explicit setup.

  • Clojure: Uses explicit recursion without predicate-based failure:

    (defn teste [lst]
      (cond (empty? lst) true
            (< (first lst) 10) (do (println (first lst)) (teste (rest lst)))
            :else (println "We stop" (rest lst))))
    (teste [1 2 11 12 13])

    The cond evaluates conditions, but subsequent instructions (e.g., (println ...)) aren’t Boolean tests that can fail the function. There’s no mechanism to backtrack to an alternative definition.

LispE’s defpred integrates backtracking and predicate logic natively, treating each instruction as a Boolean condition that must succeed for the function to proceed. This declarative approach, inspired by logic programming, contrasts sharply with the imperative or functional control flow in other Lisps, offering a distinct tool for tasks requiring conditional exploration.

  • Pattern Matching: defpat and defmacro leverage rich patterns, reducing the manual logic seen in Common Lisp’s conditionals, Scheme’s explicit branching, and Clojure’s custom dispatching.
  • Macro Flexibility: defmacro’s $ operator and pattern matching streamline transformations compared to the more verbose destructuring in other Lisps.
  • Logic Programming: defpred brings backtracking into LispE, a capability requiring significant effort to replicate in Common Lisp, Scheme, or Clojure.

These examples demonstrate LispE’s focus on expressiveness and modularity, making it a compelling option for tasks involving complex data, custom syntax, or logical reasoning. For those studying programming languages, LispE offers a fresh perspective on Lisp’s evolution.

联系我们 contact @ memedata.com