summaryrefslogtreecommitdiff
path: root/hyperquirks.lisp
blob: 70ee560acd61e91c8de029c659fec17add58544a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
;;;; hyperquirks.lisp

(in-package #:hyperquirks)

;;; MACROS

(defmacro defvarf (var &optional val doc)
  `(if (boundp ',var)
       (setf ,var ,val)
       (defvar ,var ,val ,doc)))

(defmacro imperative (&body body)
   "Evaluate expressins in BODY in sequence. Expressions that look
 like (:= VAR1 EXPR1 ...) will expand into a LET* form whose bindings
 are valid for the rest of the BODY

 E.g.

 (imperative 
   (format t \"Welcome to IMPERATIVE\") 
   (terpri)
   (:= x 10 z (+ x 20))
   (format t \"X = ~a, Z = ~a~%\" x z)
   (:= y (+ z 20))
   (format t \"Y = ~a~%\" y)
   (list x y z))

 would evaluate to:

 Welcome to IMPERATIVE    ;; <-- printed to stdout
 X = 10, Z = 30
 Y = 50
 
(10 50 30)     ;; <-- return value

IMPERATIVE introduces an implicit, anonymous BLOCK, and hence can be
returned from.
 "
   (labels ((binding-form-p (form)
              (and (consp form)
                   (keywordp (first form))
                   (eql := (first form))))
            (collect-bindings (bindings)
              (loop for (var expr . more) on bindings by #'cddr
                    collect (list var expr)))
            (expander (body)
              (cond
                ((null body) body)
                ((binding-form-p (first body))
                 (list (list* 'let* (collect-bindings (rest (first body)))
                              (expander (rest body)))))
                (t
                 (cons (first body)
                       (expander (rest body)))))))
     `(block () ,@(expander body))))

(defmacro >> (ob &rest accessors)
  "Chain access to OB."
  (let* ((tmpvar
           (gensym))
         (block-name
           (gensym))
         (body
           (loop for a in accessors
                 when (symbolp a)
                   collect `(setf ,tmpvar (funcall ',a ,tmpvar))
                 else
                   collect `(setf ,tmpvar (funcall ,a ,tmpvar)))))
    `(block ,block-name
         (let ((,tmpvar ,ob))
           ,@body))))


(defmacro ?> ((&key default (test 'null)) ob &rest accessors)
  "Chain access to OB, returning DEFAULT the first time TEST returns null.
E.g. 


> (let ((num-tree '(1 (2 3 (4 5) 6))))
    (?> num-tree second third fourth))
NIL

> (let ((num-tree '(1 (2 3 (4 5) 6))))
    (?> num-tree second third first))
4
"
  (let* ((tmpvar
           (gensym "TEMP"))
         (block-name
           (gensym "BLOCK"))
         (body
           (loop for a in accessors
                 collect `(when (funcall ',test ,tmpvar)
                            (return-from ,block-name ,default))
                 when (symbolp a)
                   collect `(setf ,tmpvar (funcall ',a ,tmpvar))
                 else
                   collect `(setf ,tmpvar (funcall ,a ,tmpvar)))))
    `(block ,block-name
         (let ((,tmpvar ,ob))
           ,@body))))


(defmacro imperative-cond (&body clauses)
  "Like cond except the first form of every clause is a binding form
alá IMPERATIVE. 

E.g. 

(imperative-cond 
   ((:= x (and (zerop (random 2)) 10)
        y 11)
    (list :x x :y y))
   (t
     12))


That would bind x and y in the first clause, check both both are non
nil, and if they are, return the evaluated body, in this case (list :x x :y y)

Otherwise 12 would be returned."

  (let ((imperative-body 
          (loop for (bindings . body) in clauses
                for vars = (unless (eq t bindings)
                             (loop for (var _ . more) on (rest bindings) by #'cddr
                                   collect var))
                collect bindings
                collect `(when (and ,@vars) (return (progn ,@body))))))
    `(imperative ,@imperative-body)))

(defmacro with-plist (keys plist &body body)
  "KEYS is a list, each member of which is either a symbol or a pair of symbols.

If a member is just a symbol, say KEY, then it is treated as the name
of a symbol-macro (defined using symbol-macrolet) that expands to the
expression (getf PLIST KEY).  In this case, KEY is not allowed to be a
keyword symbol.

If a member is a pair of symbols, it is of the form (VAR KEY). Here,
key is a valid key into the PLIST and VAR is the name of the symbol
macrolet that will be bound to (getf PLIST KEY).

EXAMPLE:

(let ((pl 
        (list 'name \"colin\" :age 40 :|currentJob| :crumb-bum)))
  (hq:with-plist (name (age :age) (job :|currentJob|)) pl 
    (setf age (1+ age)) 
    (format t \"~a the ~a had a birthday, and is now ~a years old~%\" 
            name job age) 
    pl))

The above would print out:
colin the CRUMB-BUM had a birthday, and is now 41 years old

And would return 
(NAME \"colin\" :AGE 41 :|currentJob| :CRUMB-BUM)"  

  (let* ((plist-var
           (gensym))
         (macrolet-bindings
           (loop for term in keys
                 when (consp term )
                   collect (destructuring-bind (var key) term
                               `(,var (getf ,plist-var ',key)))
                 else
                   collect `(,term (getf ,plist-var ',term)))))
    `(let ((,plist-var ,plist)) (symbol-macrolet ,macrolet-bindings ,@body))))

(defmacro with-leaves (leaf-var tree &body body)
  "Binds each atom in TREE to LEAF-VAR and then executes BODY."
  (let ((tree-var (gensym))
        (ignore-var (gensym)))
    `(let ((,tree-var ,tree))
       (tree-equal ,tree-var ,tree-var
                   :test (lambda (,leaf-var ,ignore-var)
                           (declare (ignore ,ignore-var))
                           ,@body
                           t)))))



(defmacro defun-case (name &rest clauses)
  "Clauses look like (VARLIST . BODY) 

E.g. 

(defun-case foobar 
  (() 10) 
  ((x y) (+ x y))
  ((foo) (* foo 2))
  ((a b c d) (list a b c d)))"
  (let* ((rest-args
           (gensym "variable-pattern-"))
         (clauses
           (loop for (arglist . body) in clauses
                 collect `(,(length arglist)
                           (destructuring-bind ,arglist ,rest-args
                             ,@body)))))
    `(defun ,name (&rest ,rest-args)
       (case (length ,rest-args)
         ,@clauses))))

;;; LIST FUNCTIONS

(defun group (n xs &optional default)
  "Group a list XS into consequtive sublists of size N, using the
DEFAULT to fill in any remainder in the case the length of XS is not
neatly divisible by N."
  (loop
    with len = (length xs)
    for start from 0 to len by n
    for end = (+ start n)
    when (<= end len)
      collect (subseq xs start end)
    when (< start len end)
      collect (nconc (subseq xs start)
                     (loop repeat (- end len) collect default))))

;;; STRING FUNCTIONS

(defun tabulate
    (objects line-width col-count
     &key
       (default-fill #\space)
       (stream t)
       (object-formatter "~a"))
"Print a table of OBJECTS to STREAM. The table will be LINE-WIDTH
chracters wide and each line will have COL-COUNT columns. 

The objects are formatted with the format string OBJECT-FORMATTER,
which defaults to ~a. DEFAULT-FILL is used to fill in blanks in the
table."
  (let ((row-format
          (apply 'concatenate 'string
                 "~" (prin1-to-string line-width) "<"
                 (loop for i from 1 to col-count
                       collect object-formatter
                       when (< i col-count)
                         collect "~;"
                       else
                         collect "~>~%"))))
    (dolist (g (group col-count objects default-fill))
      (apply #'format stream row-format g))))