Skip to content
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

Add `-defun' #347

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f3f29e3
Add `-defun'
nbfalcon Nov 6, 2020
6add372
`-defun': add declare with doc-string and indent
nbfalcon Nov 6, 2020
3a63e45
Add examples for `-defun'
nbfalcon Nov 6, 2020
727df5c
`-defun', `-lambda': support &optional and &rest
nbfalcon Nov 6, 2020
379e48c
Fix `-defun' &rest example
nbfalcon Nov 6, 2020
24850aa
`-lambda': support declare-forms; add `-defmacro'
nbfalcon Nov 6, 2020
2e989aa
Improve docstrings
nbfalcon Nov 6, 2020
03186e0
Fix edebug specs
nbfalcon Nov 6, 2020
6a87bae
Optimize &as bindings
nbfalcon Nov 6, 2020
95462fc
`-defun', ...: allow vectors as MATCH-FORMs
nbfalcon Nov 6, 2020
c4ffe96
`-defun', ...: optimize &as bindings in vectors
nbfalcon Nov 6, 2020
6f2626f
`-lambda', ...: improve debug specs
nbfalcon Nov 8, 2020
bcfd26a
Fix byte-compile error(s)
nbfalcon Nov 13, 2020
fd53121
`-defmacro': add example
nbfalcon Nov 13, 2020
92e623f
`dash--destructure-body': optimize: use `-let*'
nbfalcon Nov 26, 2020
23749b3
`dash--destructure-arglist': docstring generation
nbfalcon Nov 26, 2020
6afe446
Fix edebug specs: debugging `-defun'
nbfalcon Nov 26, 2020
376acdb
`-defun', ...: improve error handling
nbfalcon Nov 26, 2020
34d618b
`dash--arg-list-keywords': improve docstring
nbfalcon Nov 26, 2020
79a71d6
`-lambda': don't interpret `declare'
nbfalcon Dec 30, 2020
30a0de5
`dash-lamba-list': remove TODO
nbfalcon Dec 30, 2020
e0fb5d5
`-defun', ...: use `make-symbol'
nbfalcon Jan 6, 2021
2f9fc8c
`dash--as-matcher?': destructure directly
nbfalcon Jan 6, 2021
148a833
`dash--as-matcher?': [x &as] is not an as-matcher
nbfalcon Jan 6, 2021
83a3c12
`dash--match': refactor: use `dash--as-matcher?'
nbfalcon Jan 6, 2021
bcc9763
`dash--destructure-body': DOC -> ARGLIST
nbfalcon Jan 7, 2021
caf7445
`dash--decompose-defun-body': drop DECLARE?
nbfalcon Jan 7, 2021
7895a2b
`-defun', .... reduce code duplication
nbfalcon Jan 7, 2021
9ac1487
Drop `dash--docstring-add-signature'
nbfalcon Jan 7, 2021
d8cab22
`dash--decompose-defun-body': behave like `defun'
nbfalcon Mar 22, 2021
8ac91de
Fix square-bracket arguments
nbfalcon Mar 22, 2021
66f513e
Improve examples
nbfalcon Mar 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 63 additions & 13 deletions dash.el
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,67 @@ because we need to support improper list binding."
`(let ,inputs
(-let* ,new-varlist ,@body)))))

(defun dash--make-arglist (args)
"Make ARGS a function arglist for `dash--destructure-arglist'."
(--map-indexed
;; No need to destructure a symbol to itself.
(if (symbolp it) it (intern (format "input%d" it-index))) args))

(defun dash--progn (body-forms)
"Wrap BODY-FORMS in a `progn'.
The `progn' is omitted if it isn't needed. This function is just
`macroexp-progn' reimplemented."
(if (cdr body-forms)
`(progn ,@body-forms)
(car body-forms)))

(defun dash--destructure-arglist (args body-forms)
"Destructure function arguments ARGS using `-let'.
The result, an ELisp-form, is valid only in a function whose
arguments are the result of transforming ARGS with
`dash--make-arglist'. In the resulting form, BODY-FORMS are
executed within the bound environment."
(unless (listp args)
(signal 'wrong-type-argument "match-form must be a list"))
(let ((let-bindings
(-remove #'null
(--map-indexed
;; Symbols shouldn't be rebound; they can be taken from the
;; surrounding environment directly.
(unless (symbolp it)
(list it (intern (format "input%d" it-index))))
args))))
(if let-bindings
`(-let ,let-bindings ,@body-forms)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the -let: previously, -let* was used instead, which might be more efficient as the latter is implemented in terms of the latter. Not doing that might also make my various symbol and &as optimizations for naught. What do you think?

Copy link
Collaborator

@basil-conto basil-conto Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I can say something intelligent on this, as I have not yet looked at the code closely.

But re: -let being implemented in terms of -let* - I think that's an implementation detail that we should not base other design decisions on, since -let should ideally be implemented only in terms of itself.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to make sure that input arguments are evaluated in parallel, whether with -let or -let* doesn't matter much.

Copy link
Author

@nbfalcon nbfalcon Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wouldn't really affect the design - -let and -let* can both be used in this position. However, currently -let* is more efficient (both in terms of macro-expansion and less bytecode)::

(-lambda (in (sym &as (a . b)))) ; expands to...
(lambda ; when using `-let*` here
  (in sym)
  (let*
      ((--dash-source-20033--
        (car sym))
       (a
        (pop --dash-source-20033--))
       (b --dash-source-20033--))))
(lambda ; when using `-let` here
  (in sym)
  (let
      ((input0 sym)) ; note the needless variable
    (let*
        ((--dash-source-20039--
          (car input0))
         (a
          (pop --dash-source-20039--))
         (b --dash-source-20039--)))))

Evaluating the arguments in parallel or not doesn't make a difference here, as all binder-expressions don't reference any previously defined variables.

The trade off here is slightly better readability vs efficiency - -let makes it obvious that this won't be referring to variables defined earlier, but -let* is more efficient, currently.

Perhaps -let should be optimized more instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would need to construct some kind of graph of dependencies between the bindings. Which I suppose was too much work and I simply leveraged the built-in let to handle it for me. But yes this is a possible optimization. In practice probably insignificant :) But it could be intelectually stimulating :D

You're right about the lambda, there the parallel eval is handled by the runtime when the arguments are passed to the "wrapper lambda".

(dash--progn body-forms))))

(defun dash--decompose-defun-body (body)
"Split `defun'-body BODY into declarations and the real body.
The body of a `defun' can start with various declare forms like
interactive, `declare', ... and a docstring. Split BODY into a
list (DECLS REALBODY)."
(let* ((docstring? (car body))
(result (--split-with (memq (car-safe it) '(declare interactive))
(if (stringp docstring?) (cdr body) body))))
(when (stringp docstring?)
(push docstring? (car result)))
result))

(defmacro -defun (name match-form &rest body)
"Like `-lambda', but as a `defun'.
Define a function called NAME with destructuring.

MATCH-FORM is like in `-lambda'. Except for the (optional)
additional destructuring, this function behaves exactly like
`defun' (in terms of `declare', ...).

\(-defun NAME MATCH-FORMS &optional DOCSTRING DECL &rest BODY)"
(declare (doc-string 3) (indent 2))
(-let [(decls realbody) (dash--decompose-defun-body body)]
`(defun ,name ,(dash--make-arglist match-form)
,@decls
,(dash--destructure-arglist match-form realbody))))

(defmacro -lambda (match-form &rest body)
"Return a lambda which destructures its input as MATCH-FORM and executes BODY.

Expand All @@ -2155,19 +2216,8 @@ See `-let' for the description of destructuring mechanism."
[&optional stringp]
[&optional ("interactive" interactive)]
def-body)))
(cond
((not (consp match-form))
(signal 'wrong-type-argument "match-form must be a list"))
;; no destructuring, so just return regular lambda to make things faster
((-all? 'symbolp match-form)
`(lambda ,match-form ,@body))
(t
(let* ((inputs (--map-indexed (list it (make-symbol (format "input%d" it-index))) match-form)))
;; TODO: because inputs to the lambda are evaluated only once,
;; -let* need not to create the extra bindings to ensure that.
;; We should find a way to optimize that. Not critical however.
`(lambda ,(--map (cadr it) inputs)
(-let* ,inputs ,@body))))))
`(lambda ,(dash--make-arglist match-form)
,(dash--destructure-arglist match-form body)))

(defmacro -setq (&rest forms)
"Bind each MATCH-FORM to the value of its VAL.
Expand Down
15 changes: 13 additions & 2 deletions dev/examples.el
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ new list."
(-let (a b) (list a b)) => '(nil nil)
(-let ((a) (b)) (list a b)) => '(nil nil)
;; auto-derived match forms for kv destructuring
;;; test that we normalize all the supported kv stores
;;; test that we normalize all the supported kv stores
(-let (((&plist :foo :bar) (list :foo 1 :bar 2))) (list foo bar)) => '(1 2)
(-let (((&alist :foo :bar) (list (cons :foo 1) (cons :bar 2)))) (list foo bar)) => '(1 2)
(let ((hash (make-hash-table)))
Expand All @@ -1181,7 +1181,7 @@ new list."
(-let (((&hash? 'a) (funcall fn ht)))
a)) => '(3)
(-let (((_ &keys :foo :bar) (list 'ignored :foo 1 :bar 2))) (list foo bar)) => '(1 2)
;;; go over all the variations of match-form derivation
;;; go over all the variations of match-form derivation
(-let (((&plist :foo foo :bar) (list :foo 1 :bar 2))) (list foo bar)) => '(1 2)
(-let (((&plist :foo foo :bar bar) (list :foo 1 :bar 2))) (list foo bar)) => '(1 2)
(-let (((&plist :foo x :bar y) (list :foo 1 :bar 2))) (list x y)) => '(1 2)
Expand Down Expand Up @@ -1259,6 +1259,17 @@ new list."
(funcall (-lambda (a b) (+ a b)) 1 2) => 3
(funcall (-lambda (a (b c)) (+ a b c)) 1 (list 2 3)) => 6)

(defexamples -defun
(progn (-defun example/cdr ((_ . tail)) tail)
(example/cdr '(a . b))) => 'b
(progn (-defun example/car ((cur)) cur)
(example/car '(a . b))) => 'a
(progn (-defun example/add-cons ((a . b))
"Add the `car' and `cdr' of INPUT0."
(interactive (list (cons 1 2)))
(+ a b))
(command-execute #'example/add-cons)) => 3)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't examples limited to a single line? Have you tried regenerating the docs from this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't yet tried regenerating the docs - I'll do that later. I see some two-line examples in -setq.


(defexamples -setq
(progn (-setq a 1) a) => 1
(progn (-setq (a b) (list 1 2)) (list a b)) => '(1 2)
Expand Down