[Small-cl-src] Pattern matching in function headers

Julian Stecklina der_julian at web.de
Sat Jun 5 11:53:15 EDT 2004


Hello,

I had the idea to do pattern matching in function definitions as
Haskell allows. Any comments are welcome.

-------------- next part --------------
(defpackage "PATTERN-MATCHING"
  (:nicknames "PM")
  (:use "CL")
  (:export "DEFH" "ONE-OF" "AIF"))
(in-package "PATTERN-MATCHING")

;;; Some basic macros

(defmacro aif ((arg test) then &optional else)
  (let ((tt (gensym)))
    `(let ((,tt ,test))
       (if ,tt
	   (let ((,arg ,tt))
	     ,then)
	 ,else))))

(defmacro one-of (&rest things)
  "Constructs a function that checks whether is in the set of 'things'"
  `(lambda (x)
     (find x ',things)))

;;; Pattern matching as in Haskell

;; Variables are constructed using this read macro:
;; ?var -> #S(VARIABLE :NAME VAR :KEY NIL)
;; ?(var test) -> #S(VARIABLE :NAME VAR :KEY TEST)

;; A structure that describes a variable.
;; On the first occurrence of a variable key is a function
;; taking one argument and returning a generalised boolean. On
;; every other occurrence it takes two parameters and returns 
;; true iff they match.
(defstruct variable
  name
  (key nil))

(set-macro-character #\? 
		     (lambda (stream char)
		       (declare (ignore char))
		       (let ((form (read stream t nil t)))
			 (if (consp form)
			     (make-variable :name (first form)
					    :key (second form))
			   (make-variable :name form)))))

;; This special variable is used during pattern compilation
(defvar *vars-assigned*)

(defun generate-matcher (pattern variable)
  "Generate code to match a specific pattern in a given variable"
  (cond
   ;; If the pattern is a variable and it was already assigned
   ;; check whether the new value is equal, else fail.
   ((and (variable-p pattern)
	 (member pattern *vars-assigned*))
    (aif (key (variable-key pattern))
	 `(funcall ,key
		   ,(variable-name pattern) ,variable)
	 `(eql ,(variable-name pattern) ,variable)))
   ;; If the pattern is an unassigned variable and fits key, 
   ;; set it.
   ((variable-p pattern)
    (push pattern *vars-assigned*)
    (aif (key (variable-key pattern))
	 `(cond
	   ((funcall ,key ,variable)
	    (setq ,(variable-name pattern) ,variable)
	    t)
	   (t nil))
	 `(progn
	    (setq ,(variable-name pattern) ,variable)
	    t)))
   ;; If the pattern is NIL so must be the variable we were given
   ((null pattern)
    `(null ,variable))
   ;; Our pattern is a CONS. Let's generate code for its head
   ;; and tail.
   ((consp pattern)
    (let ((new-var (gensym)))
      `(and 
	(consp ,variable)
	(let ((,new-var (car ,variable)))
	  ,(generate-matcher (car pattern) new-var))
	(let ((,new-var (cdr ,variable)))
	  ,(generate-matcher (cdr pattern) new-var)))))
   ;; The pattern is an ATOM, it has to be equal to what we have
   ;; been given.
   ((atom pattern)
    `(eql ',pattern ,variable))
   ;; If we arrive here, somethings wrong...
   (t
    (error "Do know how to compile pattern ~S." pattern))))
       
(defun find-all-variables (pattern)
  "Find all variables in a given pattern"
  (cond
    ((variable-p pattern)
     (cons pattern nil))
    ((consp pattern)
     (nconc (find-all-variables (car pattern))
	    (find-all-variables (cdr pattern))))
    ((or (null pattern)
	 (atom pattern))
     nil)))
       
(defun compile-pattern-spec (patterns forms body not-matched)
  "Compile a pattern specification of a defh form"
  (let* ((variables (remove-duplicates
		     (mapcan #'find-all-variables patterns))))
    `(let ,(mapcar (lambda (x)
		     `(,(variable-name x) nil))
		   variables)
      (if ,(let ((*vars-assigned* nil))
		`(and ,@(loop for form-var in forms
			      and pattern in patterns
			      collect (generate-matcher pattern form-var))))
	  ,body
	  ,not-matched))))

(defun clauses-sane-p (clauses)
  "Every clause should have the same number of patterns"
  (and clauses
       (every (let ((first-len (length (first (first clauses)))))
		(lambda (x)
		  (= (length (first x)) first-len)))
	      clauses)))
		   
(defun compile-defh-body (clauses)
  "This functions returns a parameter list and body for a defh form"
  (if (not (clauses-sane-p clauses))
      (error "Malformed defh body: ~{~S~}" clauses)

      ;; So we got a correctly formed clause list
      (let ((form-vars (loop 		; Our list of parameters the function takes
			   repeat (length (first (first clauses)))
			 collect (gensym))))
	(values form-vars
		(loop for (patterns body) in (reverse clauses)
		      for code = (compile-pattern-spec patterns form-vars body
						       `(error "No pattern matched"))
		      then (compile-pattern-spec patterns form-vars body old-code)
		      for old-code = code
		      finally (return code))))))
	
(defun parse-defh-clauses (clauses)
  "Parse the raw clauses into the internal form"
  (and clauses
       (aif (pos (position-if (lambda (x) ; avoid exporting '->
				(and (symbolp x)
				     (string= "->" (string x))))
			      clauses))
	    (cons (list (subseq clauses 0 pos)
			(elt clauses (1+ pos)))
		  (parse-defh-clauses (subseq clauses (+ 2 pos))))
	    (error "Malformed defh form: ~S" clauses))))

(defmacro defh (name &rest raw-clauses)
  "Define a function based on patterns"
  (let ((parsed-clauses (parse-defh-clauses raw-clauses)))
    (multiple-value-bind (parameter body)
	(compile-defh-body parsed-clauses)
      `(defun ,name ,parameter 
	,body))))

-------------- next part --------------
(defpackage :pattern-matching-examples
  (:nicknames "PMATCH-EXAMPLES")
  (:use "CL" "PATTERN-MATCHING"))
(in-package "PMATCH-EXAMPLES")

;;; Ok, let's do something funky! H?lli, beware!

;; Implement some predicate logic stuff *g*

;; We need some basic predicates
(defun binaryp (x)
  "Checks whether the argument is a binary operator"
  (funcall (one-of and or) x))

(defun operatorp (x)
  "Checks whether the argument is an operator"
  (funcall (one-of and or not) x))

(defun quantorp (x)
  "Checks whether the argument is a quantor"
  (funcall (one-of forall exists) x))

;; Translate an expression into a very simple form with only unary
;; and binary operators (NOT/AND/OR).
(defh simplify-expression
  ;; 'Implies' and 'Equiv'(valence)
  (implies ?expr1 ?expr2) -> `(or (not ,(simplify-expression expr1)) ,expr2)
  (equiv ?expr1 ?expr2) -> `(or (and expr1 expr2) (not (or expr1 expr2)))

  ;; Pass on quantors
  (?(quantor #'quantorp) ?var ?expr) -> `(,quantor ,var ,(simplify-expression expr))
  
  ;; Pass on unary and binary operators
  (not ?expr) -> `(not ,(simplify-expression expr))
  (?(op #'binaryp) ?expr1 ?expr2) -> `(,op ,(simplify-expression expr1)
					   ,(simplify-expression expr2))

  ;; Reduce not-binary operators to binary
  (?(op #'binaryp) ?expr1 . ?rest) -> `(,op ,expr1 (,op , at rest))
  
  ;; Ignore non-matching
  ?expr -> expr)

;;; We now assume that every expression consists only of NOT/AND/OR with 
;;; atmost two arguments.

;; Perform one praenex transformation step
(defh praenex-transform
  ;; Transform negated quantors
  (not (forall ?var ?expr)) -> `(exists ,var ,(praenex `(not ,expr)))
  (not (exists ?var ?expr)) -> `(forall ,var ,(praenex `(not ,expr)))

  ;; "Lift" a quantor one level
  (?(op #'binaryp) (?(quantor #'quantorp) ?var ?expr1) 
                   ?expr2) 
     -> `(,quantor ,var ,(praenex `(,op ,expr1 
					,expr2)))
  (?(op #'binaryp) ?expr1
                   (?(quantor #'quantorp) ?var ?expr2))
     -> `(,quantor ,var ,(praenex `(,op ,expr1
					,expr2)))
  
  ;; Ignore anything else
  ?expr -> expr)
     
;; Transform an expression into praenex normal form
(defh praenex
  ;; Transform a negated expression
  (not ?expr) -> (praenex-transform `(not ,(praenex expr)))

  ;; Ignore a quantor where it does not hurt
  (?(quantor #'quantorp) ?var ?expr) -> `(,quantor ,var ,(praenex expr))

  ;; Transform 
  (?(op #'binaryp) ?expr1 ?expr2) -> (praenex-transform `(,op ,(praenex expr1)
							      ,(praenex expr2)))
  ?expr -> expr)

;; Perform one skolem transformation step
(defh skolem-transform
  (forall ?var ?expr) ?subst ?vars -> `(forall ,var ,(skolem-transform expr subst (cons var vars)))
  (exists ?var ?expr) ?subst ?vars -> (skolem-transform expr (acons var `(,(gentemp "SKOLEM") ,@(reverse vars)) subst)
					      vars)

  ;; Transform AND/OR/NOT/predicates/functors
  (?op . ?exprs) ?subst ?vars -> `(,op ,@(loop 
					   for expr in exprs
					   collect (skolem-transform expr subst vars)))
 
  ?expr ?subst ?vars -> (aif (subst (assoc expr subst))
			     (cdr subst)
			     expr))

;; Skolemize an expression in praenex normal form
(defh skolem
  ?expr -> (skolem-transform expr nil nil))

;; We would transform an arbitrary expression like this:
#+ ignore
(skolem (praenex (simplify-expression '(implies (and (p 0) (forall x (implies (p x) (p (f x)))))
						(p (f (f 0)))))))

-------------- next part --------------

Regards,
-- 
Julian Stecklina 

Signed and encrypted mail welcome.
Key-Server: pgp.mit.edu         Key-ID: 0xD65B2AB5
FA38 DCD3 00EC 97B8 6DD8  D7CC 35D8 8D0E D65B 2AB5

Any sufficiently complicated C or Fortran program
contains an ad hoc informally-specified bug-ridden
slow implementation of half of Common Lisp.
 - Greenspun's Tenth Rule of Programming


More information about the small-cl-src mailing list