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

Implement -cond-> and -cond->> macros. #349

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
46 changes: 38 additions & 8 deletions README.md
@@ -1,8 +1,4 @@
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/magnars/dash.el/CI)
[![MELPA](https://melpa.org/packages/dash-badge.svg)](https://melpa.org/#/dash)
[![MELPA Stable](https://stable.melpa.org/packages/dash-badge.svg)](https://stable.melpa.org/#/dash)

# <img align="right" src="https://raw.github.com/magnars/dash.el/master/rainbow-dash.png"> dash.el
# <img align="right" src="https://raw.github.com/magnars/dash.el/master/rainbow-dash.png"> dash.el ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/magnars/dash.el/CI)
basil-conto marked this conversation as resolved.
Show resolved Hide resolved

A modern list api for Emacs. No 'cl required.

Expand Down Expand Up @@ -282,6 +278,8 @@ Functions pretending lists are trees.
* [-some->](#-some--x-optional-form-rest-more) `(x &optional form &rest more)`
* [-some->>](#-some--x-optional-form-rest-more) `(x &optional form &rest more)`
* [-some-->](#-some---x-optional-form-rest-more) `(x &optional form &rest more)`
* [-cond->](#-cond--x-rest-clauses) `(x &rest clauses)`
* [-cond->>](#-cond--x-rest-clauses) `(x &rest clauses)`

### Binding

Expand Down Expand Up @@ -2225,7 +2223,7 @@ and when that result is non-nil, through the next form, etc.

#### -some--> `(x &optional form &rest more)`

When expr in non-nil, thread it through the first form (via [`-->`](#---x-rest-forms)),
When expr is non-nil, thread it through the first form (via [`-->`](#---x-rest-forms)),
and when that result is non-nil, through the next form, etc.

```el
Expand All @@ -2234,6 +2232,38 @@ and when that result is non-nil, through the next form, etc.
(-some--> '(1 3 5) (-filter 'even? it) (append it it) (-map 'square it)) ;; => nil
```

#### -cond-> `(x &rest clauses)`

Conditionally thread `x` through `clauses`.
Threads x (via [`->`](#--x-optional-form-rest-more)) through each form for which the
corresponding test expression is true. Note that, unlike cond
branching, [`-cond->`](#-cond--x-rest-clauses) threading does not short circuit after the
first true test expression.
Given some elisp details, current value is exposed as the symbol
`it`.

```el
(-cond-> "abc" (stringp it) (concat "def" "ghi")) ;; => "abcdefghi"
(-cond-> 10 (even? it) (/ 2) (odd? it) list) ;; => '(5)
(-cond-> '(:name "John" :age 24) (not (plist-get it :name)) (plist-put :name "Owen") (not (plist-get it :age)) (plist-put :age 40) (not (plist-get it :address)) (plist-put :address "123, Saint St.")) ;; => '(:name "John" :age 24 :address "123, Saint St.")
```

#### -cond->> `(x &rest clauses)`

Conditionally thread `x` through `clauses`.
Threads x (via [`->>`](#--x-optional-form-rest-more)) through each form for which the
corresponding test expression is true. Note that, unlike cond
branching, [`-cond->>`](#-cond--x-rest-clauses) threading does not short circuit after the
first true test expression.
Given some elisp details, current value is exposed as the symbol
`it`.

```el
(-cond->> "abc" (stringp it) (concat "def" "ghi")) ;; => "defghiabc"
(-cond->> '(1 2 3) nil (+ 10 100 1000) t (-filter 'even?)) ;; => '(2)
(-cond->> "ghi" (= 1 1) (concat "abc" "def") (= 0 1) (split-string)) ;; => "abcdefghi"
```


## Binding

Expand Down Expand Up @@ -2658,8 +2688,8 @@ These combinators require Emacs 24 for its lexical scope. So they are offered in

#### -partial `(fn &rest args)`

Takes a function `fn` and fewer than the normal arguments to `fn`,
and returns a fn that takes a variable number of additional `args`.
Take a function `fn` and fewer than the normal arguments to `fn`,
and return a fn that takes a variable number of additional `args`.
When called, the returned function calls `fn` with `args` first and
then additional args.

Expand Down
48 changes: 48 additions & 0 deletions dash.el
Expand Up @@ -1591,6 +1591,54 @@ and when that result is non-nil, through the next form, etc."
(--> ,result ,form))
,@more))))

(defmacro -cond-> (x &rest clauses)
"Conditionally thread X through CLAUSES.
Threads x (via `->') through each form for which the
corresponding test expression is true. Note that, unlike cond
branching, `-cond->' threading does not short circuit after the
first true test expression."
basil-conto marked this conversation as resolved.
Show resolved Hide resolved
(declare (debug (form body))
(indent 1))
(when (-> clauses length (% 2) (= 1))
basil-conto marked this conversation as resolved.
Show resolved Hide resolved
(error "Wrong number of arguments"))
basil-conto marked this conversation as resolved.
Show resolved Hide resolved
(-let* ((g (dash--match-make-source-symbol))
basil-conto marked this conversation as resolved.
Show resolved Hide resolved
(steps (-map
(-lambda ((test step))
`(if ,test
(-> ,g ,step)
,g))
(-partition 2 clauses))))
`(-let* ((,g ,x)
,@(-zip-lists (-cycle (list g))
(butlast steps)))
,@(if (null steps)
g
(last steps)))))
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you build up steps in reverse, e.g. with a double pop and a push per clause, then you could use car and cdr instead of last and butlast, right?

Copy link
Author

Choose a reason for hiding this comment

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

Sorry, I don't understand what you mean here. :(

Copy link
Author

Choose a reason for hiding this comment

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

Perhaps I should make myself more clear:
I do understand the the general idea, how to use the built-in functions and why using them is a good idea.
What I'm not sure is the sequence of steps needed to achieve this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Does the following make sense?

(defun dash--cond-thread (name thread expr clauses)
  "Implement macroexpansion of NAME, one of `-cond->' or `-cond->>'.
THREAD is the corresponding threading macro, one of `->' or
`->>'.  EXPR and CLAUSES are as in `-cond->'.  Signal
`wrong-number-of-arguments' if the number of CLAUSES is odd."
  (cond
   ((null clauses) expr)
   ((let ((len (length clauses)))
      (when (= (% len 2) 1)
        (signal 'wrong-number-of-arguments (list name (1+ len))))))
   ((let ((g (make-symbol "g"))
          binds)
      (while clauses
        (let ((test (pop clauses))
              (form (pop clauses)))
          (push `(,g (if ,test (,thread ,g ,form) ,g)) binds)))
      `(let* ((,g ,expr)
              ,@(nreverse (cdr binds)))
         ,(cadr (car binds)))))))

Then -cond-> and -cond->> can be implemented as follows:

(defmacro -cond-> (expr &rest clauses)
  ;; ...
  (dash--cond-thread #'-cond-> #'-> expr clauses))

(defmacro -cond->> (expr &rest clauses)
  ;; ...
  (dash--cond-thread #'-cond->> #'->> expr clauses))

Key differences:

  • If there are no clauses, just return the first argument instead of signalling an error.
  • signal expects two arguments.
  • No need for -let; plain let will do.

Copy link
Author

Choose a reason for hiding this comment

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

It's amazing!! I wish I had thought of this.
Should I close this PR so you can open another one?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If you think my suggestion is useful and clear enough, then feel free to incorporate it in your own code for this PR.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you!
I'm incorporating the part where steps are built whit two pop and a push, and the key differences listed above. But if I incorporated all the code, I'd be taking credit for your work. Perhaps you could update over this PR eventually.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But if I incorporated all the code, I'd be taking credit for your work.

But my work is already built on top of your work. ;) Also, my public suggestions are not copyrighted and I don't care about attribution.

Perhaps you could update over this PR eventually.

Sure, if that's what you prefer, I'd be happy to oblige.

Copy link
Author

Choose a reason for hiding this comment

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

But my work is already built on top of your work. ;)

That's a nice way of looking at it.

Sure, if that's what you prefer, I'd be happy to oblige.

IMO, this is the best option :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool, then I think we're just waiting for your CA process to complete, and we should be good to go.

BTW, if you rebase this branch off the latest master then the CI tests should stop wetting the bed. :)


(defmacro -cond->> (x &rest clauses)
"Conditionally thread X through CLAUSES.
Threads x (via `->>') through each form for which the
corresponding test expression is true. Note that, unlike cond
branching, `-cond->>' threading does not short circuit after the
first true test expression."
(declare (debug (form body))
(indent 1))
(when (-> clauses length (% 2) (= 1))
(error "Wrong number of arguments."))
basil-conto marked this conversation as resolved.
Show resolved Hide resolved
(-let* ((g (dash--match-make-source-symbol))
(steps (-map
(-lambda ((test step))
`(if ,test
(->> ,g ,step)
,g))
(-partition 2 clauses))))
`(-let* ((,g ,x)
,@(-zip-lists (-cycle (list g))
(butlast steps)))
,@(if (null steps)
g
(last steps)))))

(defun -grade-up (comparator list)
"Grade elements of LIST using COMPARATOR relation, yielding a
permutation vector such that applying this permutation to LIST
Expand Down