-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lambda-list pattern #12
Conversation
I am inactive untill the deadline of a conference paper, so I can't verify this commit until then. sorry for the delay! |
Sweet. I just timed it and lambda-list beats destructuring-bind in SBCL by a factor of 3 :D |
that's amazing :o |
Done. I realized I have no way for binding patterns lexically though. Edit: (destructuring-bind (a &optional (b a)) ...) |
patterns, lexically? I don't get it. |
1 more day before the deadline, I will reconsider that later. I will merge it anyways. Thank you for the contribution! |
Finally I have the time to test this by myself! What kind of benchmark script are you using? I want to add the results to the benchmarking pages on the wiki. |
Well, I reimplemented destructuring-pattern based on your logic in #16 . A few improvements include:
While the standard &optional syntax is (var default supplied-p), it supports (pattern default supplied-p). (defclass person ()
((name :initarg :name :reader name)
(age :initarg :age)))
(match (list (make-instance 'person :name :guicho) (vector :mar 28 1990))
((lambda-list (person name) &optional ((and birthday (vector month day year)) (vector :jan 1 1970)))
(list name birthday month day year)))
;; --> (:GUICHO #(:MAR 28 1990) :MAR 28 1990)
(match (list (make-instance 'person :name :guicho))
((lambda-list (person name) &optional ((and birthday (vector month day year)) (vector :jan 1 1970)))
(list name birthday month day year)))
;; --> (:GUICHO #(:JAN 1 1970) :JAN 1 1970) |
I didn't do extensive benchmarking, but these two patterns are definitely faster than SBCL's destructuring-bind, (let ((lst '(1 :b 2)) (tmp 0))
(time (dotimes (_ 1000)
(match lst
((λlist a &key b) (incf tmp b)))))
(time (dotimes (_ 1000)
(destructuring-bind (a &key b) lst
(incf tmp b)))))
(let ((lst '(1 (2 3))) (tmp 0))
(time (dotimes (_ 1000)
(match lst
((λlist a (λlist b c)) (incf tmp b)))))
(time (dotimes (_ 1000)
(destructuring-bind (a (b c)) lst
(incf tmp b))))) While the new keyword pattern compiler is theoretically less efficient than the earlier quasi-destructive getf! one, it seems to be faster if the number of keyword arguments are low. (SBCL too seems to use a similar backend.) |
Hmm, are you okay with syntactic differences between, (lambda-list a &optional b)
(lambda-list a &optional ((list 2 3))) Is there a similar way by which patterns can be matched to keyword arguments ? |
in standard CL, the semantics of &key is itself complex: &key var | ( { var | (keywordname var) } [default [suppliedp]]). Nesting a pattern to a keyword parameter is available only to the (keywordname var) form, since otherwise it cannot specify which keyword can be used. So, although it is slightly complex, we can use the following:
Otherwise only a simple variable-binding is allowed, same as the previous implementation:
I now see why the past code uses getf!, it is to avoid the use of iterative getf which is O(n^2) if I'm correct? Sorry for not noticing this, but I was skeptical if the additional consing by copy-list would not harm the performance. It would be possible to intelligently switch between two behaviors depending on the length of the keyword params. |
For optional arguments, you don't need additional parenthesis since the position is clear.
|
sorry I was wrong, it should be ((list 2 3)) as you pointed out. yes, this is required because it should distinguish between (var default suppliedp) and (list 2 3). In addition, ((list 2 3) default suppliedp) works. |
Another thing I noticed is the special handling of guard in the previous code. Guard is a level2 pattern which might be already compiled away into guard1 (level1 pattern), so there is no point in handling them. |
https://github.com/guicho271828/trivia/blob/master/level2/derived3.lisp#L161 |
The getf! thing was popping out keywords as it found them. Actually, come to think of it, it probably wasn't even better off theoretically. Linear time would be cool, but I'm not sure if that can be accomplished without the use of hash-tables or some other clever data-structure. I should've taught about this before implementing. The new pattern is definitely much cleaner :) The new keyword pattern matching syntax is confusing, but I don't see how you can do something better. Thanks for cleaning up the implementation :) |
The special handling of 'guard' was meant to handle patterns on &optional/&key variables; the new syntax is definitely better. Also, can sub-patterns be compiled before the parent ? |
you can, by (mapcar #'pattern-expand-all subpatterns) , but they are fully expanded in the end anyways, so it wont be necessary. Only the fundamentally complex patterns like |
This is cool, ~x6 speedup?
|
Sweet. Yeah, that's about the speedup I get as well. It's kind strange that we do indeed see a speedup. SBCL's destructuring-bind is essentially doing the same thing as lambda-list. |
Aren't patterns also 'lexically scoped' ? Calling pattern-expand inside a pattern breaks that rule (vis a vis macros). |
"lexically" and "scope" is a word for variables. A pattern may contain variables but it may also contain others. pattern-expand is an analogue to macroexpand, so this is just a compile-time utility and it does nothing related to runtime, just in case. |
Macros too are bound lexically at compile-time. Macros/Macrolets are written To extend the analogy, I was assuming a 'patternlet' for guard inside lambda-list |
alright, now I understand what you mean, yes variable patterns are lexically bound in that sense. For example,
returns t. ( |
first-order "local" patterns are not currently assumed, but thanks to the inner use of lisp-namespace (yes, another work of mine) which provides a convenient way to bind pattern definition (dynamically, though), it is possible to implement https://github.com/guicho271828/lisp-namespace Edit: Oh I now remember that I already have defined |
hm, not actually. your aim is something like this?
it should work in compile time, so it introduces another difficulty. the
|
Ah but pattern-let is defined before calling; I was imagining a re-binding within the pattern itself. Previously, lambda-list 'implicitly' redefined 'guard', such that (guard (var &optional default supplied) ...) did not follow the semantics of the canonical pattern. This came with the assumption that lambda-list could lexically rebind 'guard' locally, without affecting the pattern-expansion. Basically, is there a guarantee that pattern-expand is called on a tree before being called on its children (assuming that no pattern explicitly calls pattern-expand-1 on any of its child patterns) ? For example, (lambda-list a &optional (guard (b 2) (eql b 'a))) would have been valid if 'lambda-list' were expanded before 'guard' (because of the implicit lexical rebinding). It would've raised an error if 'guard' was expanded before 'lambda-list'. Is 'commutativity', in the sense that of order of pattern expansion, a requirement ? I can imagine the trivia-optimizer breaking this assumption, but the answer seems to be no otherwise. |
interesting topic... patterns are basically meant to be expanded in normal order (as in macros) -- expanded from the top, left to right. I wrote Although it is maintained, this is unintentional, and I think breaking the commutativity is ok. Indeed, breaking the commutativity is required for implementing a "local pattern" like this (same as what I posted previously):
This requires pattern-expand-all because, if I replace Optimizer would not see the unexpanded patterns. The input to the optimizer should consist of |
Ah, you're right; pattern-let does exactly what I meant. I guess the rest of it makes sense. It'd be cool have consistent local-patterns though (without commutativity). Would using labels instead of macrolet in namespace-let, fix this ? There also seems to be a bug in lisp-namespace; the macro (trivia.level2.impl::pattern-let ((vec1 (lambda (x) `(vector ,x))))
(pattern-expand-all arg)) expands into, (PROGN
(LET ((#:TEMP835 (LAMBDA (X) `(VECTOR ,X))))
(DECLARE (TYPE (PATTERN-TYPE) #:TEMP835))
(MACROLET ((SYMBOL-PATTERN (&WHOLE LISP-NAMESPACE::WHOLE LISP-NAMESPACE::X)
(IF (EQUAL LISP-NAMESPACE::X ''VEC1)
'#:TEMP835
LISP-NAMESPACE::WHOLE)))
(PROGN (PATTERN-EXPAND-ALL ARG))))) The type declaration here is missing the package information for some reason. |
Thanks for pointing it out, fixed now.
|
Now I added the longed See another example using
Now |
Your idea on local patterns is so interesting, because it gives a way to add a convenient local pattern for arrays. See, for example, how such a pattern could implement LU-decomposition beautifully (if given additional interface ;; array : N*N input matrix
;; LU : N*N result matrix
;; kth iteration: processing bottom-right (N-k)*(N-k) sub-matrix
(match* (array LU)
(((array (element k k ckk-a) (displaced k (k N) ck*-a)
(displaced (k N) k c*k-a) (displaced (k N) (k N) c**-a))
(array (element k k ckk-lu) (displaced k (k N) ck*-lu)
(displaced (k N) k c*k-lu) (displaced (k N) (k N) c**-lu)))
(array-setf c*k-lu (map 'vector (lambda (cjk) (/ cjk ckk)) c*k-a))
(array-setf c**-lu (matrix-sub c**-a (elementwise-prod ck*-a c*k-lu))))) |
Hmm, but that can probably be implemented with just a defpattern no ? I mean the patterns don't really bind previously defined patterns. Digression: (defun recursive-lu! (M &optional (ret M))
(if (< (dimensions M 0) 2) ret
(recursive-lu! #i(ger!(-1, M[1:, 0], scal!(/ M[0, 0], M[0, 1:]), M[1:, 1:])) ret))) |
I'm not familier with matrix libraries, but that seems useful and interesting. thanks! |
This is a first pass at a lambda-list pattern. It works, but I think there currently are three issues,
i) I've used the 'variable-symbol' type instead of 'variablep' in order to exclude keywords and lambda-list-keywords.
ii) I had resort to the ugly trick of creating list and then binding them inside patterns using '<>'; it's likely that you can think of a better way to accomplish this (in function compile-destructuring-pattern).
iii) The loops inside recurse-maadi are not upper-bound.