6.1 Pattern Functions - naver/lispe GitHub Wiki

Pattern Functions: defpat

defpat stands for: function pattern definition.

defpat helps define functions that can share the same name but differs on their parameter definition (polymorphism). LispE will choose the first function in the list that matches your arguments.

(defpat name (P1...Pn) ...)

A pattern function can take as many parameters (Pi) as you want (well... actually up to 15). A parameter is defined as followed:

Parameter Definition Rules

  1. P -> (type P1...Pn) where type is a data type or a basic type
  2. P -> (condition v1...vN P1) where condition is either a function call or a basic instruction. v1...vn are values, there can be only one parameter, always at the end of the condition
  3. P -> atom a simple variable, which can be unified with an argument.
  4. P -> value that should match the argument. Note that value can be a list, if the first element of that list is not a type, a function or an instruction.

Of course, all these definition can be nested one into the others.

Important: The order in which functions are applied is constrained by the presence of a type on the first parameter. If this type is missing, when P is only a condition or an atom, then this function is considered as a fallback and will only be applied once all the corresponding typed functions have been tested. The selection is done according to the argument type...

; The famous FIZZBUZZ example

; check if the rest of v/d is 0
(defun checkmod (v d) (eq (% v d) 0))

; We have here a nested declaration:
; we start with: P -> (type P)
; P   -> (integer_ P')
; P'  -> (checkmod v P'')
; P'' -> atom
; hence: (integer_ (checkmod value atom))

; if the element can be divided by 15 we return fizzbuzz
(defpat fizzbuzz ( [integer_ (not (% x 15))] ) 'fizzbuzz)

; if the element can be divided by 3 we return fizz
(defpat fizzbuzz ( [integer_ (checkmod x 3)] ) 'fizz)

; if the element can be divided by 5 we return buzz
(defpat fizzbuzz ( [integer_ (checkmod x 5)] ) 'buzz)

; rollback function, no type, we return x
; This function will only be called, if none of the above did apply
(defpat fizzbuzz (x) x)

; we apply 'fizzbuzz' to the first 100 elements.
; The argument type is: integer_

(mapcar 'fizzbuzz (range 1 100 1))

Note that if you use a macro instead of a function, the last element of the macro should also be a variable that will be the one with which the argument will be unified.

; We replace 'checkmod' with a macro...

(defmacro checkmod (var val) (eq 0 (% var val)))

; Since checkmod is a macro, it will be inserted in each function parameter list,
; which imposes that the last parameter be the variable
; that will be unified with the argument: here var

; Hence, after application of the macro,
; fizzbuzz functions would have the following actual implementation:

(defpat fizzbuzz ( [integer_ (eq 0 (% x 3)) ] ) 'fizz)

With Data Type

Of course, a pattern function can accept data types as parameters:


; Let's define our data type first

(data (Circle integer_) (Rectangle integer_ integer_) )

; We define the surface function according to the object type:
; Here is the surface of a circle: 
(defpat Surface ([Circle r]) (* _pi r r))

; Here is the surface of a rectangle
(defpat Surface ([Rectangle w h]) (* w h))

; The triangle is equilateral here, one single value is enough
(data Shape (Triangle _) (Square _))

; We define a pattern function that takes a Shape as argument
(defpat dim( [Shape dimension] )
    (println 'Dimension dimension)
)

We have defined our data type together with two definitions for Surface, one for a circle, the other one for a rectangle. Now, the system will automatically choose the right function according to its arguments:


(println (Surface [Circle 10])) yields : 314.159

; If we provide a Rectangle structure

(println (Surface [Rectangle 10 20] )) yields: 200

; These two calls do match as they belong to Shape 

(dim (Triangle 10))
(dim (Square 20))

When you define your function, you can also replace some fields with a nil.


(defpat Width ( [Rectangle w _] ) w)

The second parameter for Rectangle is not necessary here, it is simple skipped.

Enforcing Argument Type

First, the basic types are actually all implemented as basic data type, which means that you can use them to add a constraint on the argument types. The implementation is exactly the same as above, you simply replace your data type name with the required type.


(defpat addnumbers ( [integer_ x] [integer_ y] )
       (+ x y)
)

(addnumbers 10 20) ; is valid
(addnumbers 10 'x) ; is invalid

Checking Argument Through a Function

It is actually possible to force an argument to match a pattern function through a call to a function that will either return true or nil.

In particular, one might want to check a regular expression pattern on an argument to trigger the execution of a function.

Again, the formalism is the same as above, we simply replace the data type with a function call.

IMPORTANT When using a function in a pattern, the argument that is used is always the last one in the argument list.


; This method compares two elements in a list
(defun checkless (x) (< (car x) (cdar x)))

; This pattern method expects the first argument to comply
; with the regular expression: \d\d (two digits)
(defpat action ( [prgx `\d\d` x] u)
   (println 'ok u)
)

; This pattern method expects the first argument to comply
; with the regular expression: %a+ (a sequence of alphabetical characters)
(defpat action ( [rgx `%a+` x] u)
   (println 'Alpha u)
)

; This pattern checks if the list x complies with checkless
(defpat action ( [checkless x] u)
   (println 'Yooo u)
)

; these three calls work fine
(action '(200 220) 'first)
(action "12" 'finally)
(action "Test" 'thing)

; this one fails: checkless fails
(action '(280 220) 'first)

The following example demonstrates a case when the comparison is not in the right order:


; The last element in the argument list is 's'
(defpat parcours ([string_ (in s "restaurant")] )
   (println 'ok s)
)

(defpat parcours ([string_ s])
     (println "No restaurant")
)

(parcours "la ville contient un bon restaurant italien")

Note: An important remark should be made on the above example. When you define a pattern function, the type of the first element of the definition is used as a key to access these functions. This type should be either a basic type (integer_, string_ list_ etc.) or a data type, otherwise it is nil. If we remove the 'string_' definition in the first function, then it will be considered as a callback function, which then will be indexed on 'nil'. By putting this constraint, we add this first function to the list of functions that can apply to a string

Pattern Matching against Lists and Dictionaries

It is of course possible to match a structure against dictionaries and lists.

; With dictionaries
(defpat tst({"12":y x:y})
   (println  x y)
)

; This call will display: bidule and machine
; "12" triggers the unification of y with "machine" 
; and only "bidule" as a key matches "machine" as a value
(tst {"12":"machine" "truc":"machin" "bidule":"machine"})

The separator operator: $

It is in particular possible to use the $ operator to match against sub-lists. This operator can be used within a pattern parameter. It tries to match the left part of the parameter list with the list argument. The rest of the list is then stored into the final variable as demonstrated with the following code:


; 1. A parameter pattern, `z` is the rest of the list
(defpat tst ( [x y $ z] )
   (println x y z)
)

; 2. The list should contain only one element
(defpat tst ((x))
   (println 'unique x)
)

; 3. matching against the empty list
(defpat tst ( ())
   (println 'empty)
)

(tst ()) ; triggers 3. and displays empty
(tst '(1 2 3 4 5)) ; triggers 1. and displays: 1 2 (3 4 5)
(tst '(1 2)) ; triggers 1. and displays: 1 2 ()
(tst '(1)) ; triggers 2. and displays: unique 1

The $ Operator in Dictionaries

The $ operator can also be used with dictionaries. However, in this case it is considered as a key.


; the $ is followed with the :

(defpat tst({_:y _:y $:z})
   (println y z)
)


(setq d {"12":"machine" "truc":"machin" "bidule":"machine"})

; displays: machine {"truc":"machin"}
(tst d)

Kleene Operators: + * %

The Pattern Functions also accept the used of the Kleene Operators with variables or function constraints.

With Variables

If a variable is associated with a Kleene operator, then the next element in the pattern will be used as a constraint.

  • 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

; the "]" is used as a constraint on the right
(defpat test( ["[" a* "]" $ res] )
   (println a res)
)

(test '("[" a b c d e f "]" i j k)) ; displays (a b c d e f) (i j k)

With Conditions

It also possible to add a Kleene operator to a condition. In that case, the constraint only bears on the current element.

; This function will gather all the elements that comply with the constraint
; and will store the rest in r
(defpat nm ( [ ">" (< a 31)+ $ r])
   (println a r)
)
(nm '(">" 1 2 3 10 20 30 40 50 70)) ; displays (1 2 3 10 20 30) (40 50 60)