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

bug: -if-let mismatch between documentation and behavior #392

Open
ethan-leba opened this issue Nov 18, 2021 · 5 comments
Open

bug: -if-let mismatch between documentation and behavior #392

ethan-leba opened this issue Nov 18, 2021 · 5 comments
Labels
documentation Pertains to project documentation
Milestone

Comments

@ethan-leba
Copy link

ethan-leba commented Nov 18, 2021

The documentation for -if-let reads as follows: If VAL evaluates to non-nil, bind it to VAR and do THEN, otherwise do ELSE.

However, -if-let's behavior does not match this, and instead appears to match
against (part of) var:

(-if-let ((foo . bar) '(3)) 'true 'false) ;; evals to 'false
(if '(3) 'true 'false) ;; evals to 'true

;; if-let macroexpands to:
(let ((--dash-source-98-- '(3)))
  (if --dash-source-98--
      (let ((foo (pop --dash-source-98--)))
        (if foo ;; should be --dash-source-98--
            (let ((bar --dash-source-98--))
              (if bar 'true 'false))
          'false))
    'false))

Thanks for the great package!

@Fuco1
Copy link
Collaborator

Fuco1 commented Nov 20, 2021

The VAL here is '(3) is non-nil, which passes. The list matching was done on purpose like this, where you can match prefixes or extra keys. That happened before if-let was created though so the logic is a bit weird maybe. Consider these examples

(-let (((a b c d) nil))                         ;; only this one would fail -if-let
  (message "%s" (list a b c d)))

(-let (((a b c d) (list 1)))
  (message "%s" (list a b c d)))

(-let (((a b c d) (list 1 2 3 4 5)))
  (message "%s" (list a b c d)))

@Fuco1 Fuco1 added the documentation Pertains to project documentation label Nov 20, 2021
@ethan-leba
Copy link
Author

The VAL here is '(3) is non-nil, which passes.

By passes do you mean (-if-let ((foo . bar) '(3)) 'true 'false) evaluates to 'true? It evaluates to 'false in my testing -- sorry if the code comments in my original post were unclear!

(-let (((a b c d) nil))                         ;; only this one would fail -if-let
  (message "%s" (list a b c d)))

(-let (((a b c d) (list 1)))
  (message "%s" (list a b c d)))

(-let (((a b c d) (list 1 2 3 4 5)))
  (message "%s" (list a b c d)))

This fails -if-let:

(-if-let ((a b c d) (list 1))
    (message "%s" (list a b c d)))

So -if-let appears to be performing it's conditional against some part of the destructured variables. This shouldn't be the case, correct?

@Fuco1
Copy link
Collaborator

Fuco1 commented Nov 20, 2021

Oh yea, so I got myself confused.

What you say is correct. The -if-let requires all the destructured variables to be non-nil, not the "source". What I said was not wrong but unrelated :D

We should fix the documentation.

There are cases where for example (list nil nil) is actually truthy but destructuring it to (a b) would be false with -if-let. So this needs to be taken into consideration by people using it.

@ethan-leba
Copy link
Author

ethan-leba commented Nov 20, 2021

Gotcha, so that's the expected behavior then. Maybe something like: If all VARs bound by evaluating VAL are non-nil, do THEN, otherwise do ELSE.? -when-let seems to have the same behavior as well.

@basil-conto basil-conto added this to the 2.19.2 milestone Apr 4, 2022
@basil-conto basil-conto added this to To Do in Release 2.20.0 Apr 4, 2022
@yuhan0
Copy link

yuhan0 commented Jun 18, 2023

This was actually confusing coming from Clojure, where if-let does indeed work the way the current docstring implies.

Here's the equivalent example (note: gensyms cleaned up for readability)

(if-let [[foo bar] '(3)] :true :false) ;; => :true

;; macroexpands to:
(let [temp# '(3)]
  (if temp#
    (let* [vec# temp#
           foo  (nth vec# 0 nil)
           bar  (nth vec# 1 nil)]
      :true)
    :false))

Clojure also has a related set of macros if-some and when-some for checking that the value is strictly non-nil (since it has both false and nil to be falsey values).
Given that we shouldn't make any breaking changes, maybe we could repurpose those names here in Dash for macros that actually behave this way? And add a warning to the docstring of -if-let, pointing out the difference.

(defmacro -if-some (var-val then &rest else)
  "If VAL evaluates to non-nil, bind it to VAR and do THEN,
otherwise do ELSE.

Note: binding is done according to `-let'.

\(fn (VAR VAL) THEN &rest ELSE)"
  (declare (debug ((sexp form) form body))
           (indent 2))
  (let* ((sym (make-symbol "input0"))
         (bindings (dash--match (car var-val) sym)))
    `(let ((,sym ,(cadr var-val)))
       (if ,sym
           (let* ,bindings
             ,then)
         ,@else))))
(-if-some ((a b) '(3))
    (list a b)
  'false)
;; => (3 nil)

;; =macroexpand=>
(let ((input0 '(3)))
  (if input0
      (let* ((--dash-source-97-- input0))
             (a (pop --dash-source-97--))
             (b (car --dash-source-97--)))
        (list a b))
    'false))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Pertains to project documentation
Projects
Development

No branches or pull requests

4 participants