diff --git a/docs/language/api.rst b/docs/language/api.rst index 91bf707..3ee2374 100644 --- a/docs/language/api.rst +++ b/docs/language/api.rst @@ -823,26 +823,47 @@ keyword, the second function would have raised a ``NameError``. (set-a 5) (print-a) -if / if-not ------------ +if / if* / if-not +----------------- .. versionadded:: 0.10.0 if-not -``if`` is used to conditionally select code to be executed. It has to contain a -condition block and the block to be executed if the condition block evaluates -to ``True``. Optionally, it may contain a final block that is executed in case -the evaluation of the condition is ``False``. +``if / if* / if-not`` respect Python *truthiness*, that is, a *test* fails if it +evaluates to a "zero" (including values of ``len`` zero, ``nil``, and +``false``), and passes otherwise, but values with a ``__bool__`` method +(``__nonzero__`` in Python 2) can overrides this. -``if-not`` is similar, but the second block will be executed when the condition -fails while the third and final block is executed when the test succeeds -- the -opposite order of ``if``. +The ``if`` macro is for conditionally selecting an expression for evaluation. +The result of the selected expression becomes the result of the entire ``if`` +form. ``if`` can select a group of expressions with the help of a ``do`` block. + +``if`` takes any number of alternating *test* and *then* expressions, plus an +optional *else* expression at the end, which defaults to ``nil``. ``if`` checks +each *test* in turn, and selects the *then* corresponding to the first passed +test. ``if`` does not evaluate any expressions following its selection, similar +to the ``if/elif/else`` control structure from Python. If no tests pass, ``if`` +selects *else*. + +The ``if*`` special form is restricted to 2 or 3 arguments, but otherwise works +exactly like ``if`` (which expands to nested ``if*`` forms), so there is +generally no reason to use it directly. + +``if-not`` is similar to ``if*`` but the second expression will be executed +when the condition fails while the third and final expression is executed when +the test succeeds -- the opposite order of ``if*``. The final expression is +again optional and defaults to ``nil``. Example usage: .. code-block:: clj - (if (money-left? account) + (print (if (< n 0.0) "negative" + (= n 0.0) "zero" + (> n 0.0) "positive" + "not a number")) + + (if* (money-left? account) (print "let's go shopping") (print "let's go and work")) @@ -850,9 +871,6 @@ Example usage: (print "let's go and work") (print "let's go shopping")) -Python truthiness is respected. ``None``, ``False``, zero of any numeric type, -an empty sequence, and an empty dictionary are considered ``False``; everything -else is considered ``True``. lif and lif-not diff --git a/hy/compiler.py b/hy/compiler.py index 2dd13c4..8032153 100644 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -999,7 +999,7 @@ class HyASTCompiler(object): name=name, body=body) - @builds("if") + @builds("if*") @checkargs(min=2, max=3) def compile_if(self, expression): expression.pop(0) @@ -1011,7 +1011,7 @@ class HyASTCompiler(object): if expression: orel_expr = expression.pop(0) if isinstance(orel_expr, HyExpression) and isinstance(orel_expr[0], - HySymbol) and orel_expr[0] == 'if': + HySymbol) and orel_expr[0] == 'if*': # Nested ifs: don't waste temporaries root = self.temp_if is None nested = True diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy index 6667ed8..8d8a3f8 100644 --- a/hy/core/bootstrap.hy +++ b/hy/core/bootstrap.hy @@ -25,12 +25,20 @@ ;;; These macros are the essential hy macros. ;;; They are automatically required everywhere, even inside hy.core modules. +(defmacro if [&rest args] + "if with elif" + (setv n (len args)) + (if* n + (if* (= n 1) + (get args 0) + `(if* ~(get args 0) + ~(get args 1) + (if ~@(cut args 2)))))) (defmacro macro-error [location reason] "error out properly within a macro" `(raise (hy.errors.HyMacroExpansionError ~location ~reason))) - (defmacro defn [name lambda-list &rest body] "define a function `name` with signature `lambda-list` and body `body`" (if (not (= (type name) HySymbol)) @@ -39,7 +47,6 @@ (macro-error name "defn takes a parameter list as second argument")) `(setv ~name (fn ~lambda-list ~@body))) - (defmacro let [variables &rest body] "Execute `body` in the lexical context of `variables`" (if (not (isinstance variables HyList)) diff --git a/hy/core/macros.hy b/hy/core/macros.hy index 5d52ed3..d39aabe 100644 --- a/hy/core/macros.hy +++ b/hy/core/macros.hy @@ -159,20 +159,25 @@ ret) -(defmacro if-not [test not-branch &optional [yes-branch nil]] +(defmacro if-not [test not-branch &optional yes-branch] "Like `if`, but execute the first branch when the test fails" - (if (nil? yes-branch) - `(if (not ~test) ~not-branch) - `(if (not ~test) ~not-branch ~yes-branch))) + `(if* (not ~test) ~not-branch ~yes-branch)) -(defmacro lif [test &rest branches] +(defmacro lif [&rest args] "Like `if`, but anything that is not None/nil is considered true." - `(if (is-not ~test nil) ~@branches)) + (setv n (len args)) + (if* n + (if* (= n 1) + (get args 0) + `(if* (is-not ~(get args 0) nil) + ~(get args 1) + (lif ~@(cut args 2)))))) -(defmacro lif-not [test &rest branches] + +(defmacro lif-not [test not-branch &optional yes-branch] "Like `if-not`, but anything that is not None/nil is considered true." - `(if (is ~test nil) ~@branches)) + `(if* (is ~test nil) ~not-branch ~yes-branch)) (defmacro when [test &rest body] diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py index 3a1f92d..41156a3 100644 --- a/tests/compilers/test_ast.py +++ b/tests/compilers/test_ast.py @@ -73,15 +73,15 @@ def test_ast_bad_type(): def test_ast_bad_if(): - "Make sure AST can't compile invalid if" - cant_compile("(if)") - cant_compile("(if foobar)") - cant_compile("(if 1 2 3 4 5)") + "Make sure AST can't compile invalid if*" + cant_compile("(if*)") + cant_compile("(if* foobar)") + cant_compile("(if* 1 2 3 4 5)") def test_ast_valid_if(): - "Make sure AST can't compile invalid if" - can_compile("(if foo bar)") + "Make sure AST can compile valid if*" + can_compile("(if* foo bar)") def test_ast_valid_unary_op(): @@ -539,13 +539,13 @@ def test_invalid_list_comprehension(): def test_bad_setv(): """Ensure setv handles error cases""" - cant_compile("(setv if 1)") + cant_compile("(setv if* 1)") cant_compile("(setv (a b) [1 2])") def test_defn(): """Ensure that defn works correctly in various corner cases""" - cant_compile("(defn if [] 1)") + cant_compile("(defn if* [] 1)") cant_compile("(defn \"hy\" [] 1)") cant_compile("(defn :hy [] 1)") can_compile("(defn &hy [] 1)") @@ -561,5 +561,5 @@ def test_setv_builtins(): (defn get [self] 42) (defclass B [] (defn get [self] 42)) - (defn if [self] 0)) + (defn if* [self] 0)) """) diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy index 9986ff7..8c7533a 100644 --- a/tests/native_tests/language.hy +++ b/tests/native_tests/language.hy @@ -274,6 +274,32 @@ (assert (= (cond) nil))) +(defn test-if [] + "NATIVE: test if if works." + ;; with an odd number of args, the last argument is the default case + (assert (= 1 (if 1))) + (assert (= 1 (if 0 -1 + 1))) + ;; with an even number of args, the default is nil + (assert (is nil (if))) + (assert (is nil (if 0 1))) + ;; test deeper nesting + (assert (= 42 + (if 0 0 + nil 1 + "" 2 + 1 42 + 1 43))) + ;; test shortcutting + (setv x nil) + (if 0 (setv x 0) + "" (setv x "") + 42 (setv x 42) + 43 (setv x 43) + (setv x "default")) + (assert (= x 42))) + + (defn test-index [] "NATIVE: Test that dict access works" (assert (= (get {"one" "two"} "one") "two")) diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy index 0d10609..f833a30 100644 --- a/tests/native_tests/native_macros.hy +++ b/tests/native_tests/native_macros.hy @@ -190,11 +190,11 @@ (defn test-lif [] "test that lif works as expected" - ; nil is false + ;; nil is false (assert (= (lif None "true" "false") "false")) (assert (= (lif nil "true" "false") "false")) - ; But everything else is True! Even falsey things. + ;; But everything else is True! Even falsey things. (assert (= (lif True "true" "false") "true")) (assert (= (lif False "true" "false") "true")) (assert (= (lif 0 "true" "false") "true")) @@ -202,7 +202,14 @@ (assert (= (lif "" "true" "false") "true")) (assert (= (lif (+ 1 2 3) "true" "false") "true")) (assert (= (lif nil "true" "false") "false")) - (assert (= (lif 0 "true" "false") "true"))) + (assert (= (lif 0 "true" "false") "true")) + + ;; Test ellif [sic] + (assert (= (lif nil 0 + nil 1 + 0 2 + 3) + 2))) (defn test-lif-not [] "test that lif-not works as expected"