Repository: hylang/hy

Issue:
Distinguishing `if`, `when`, and `cond`
Let's talk about how exactly these three similar constructs should differ.

`cond` requires brackets around each condition-result pair. Previously the idea of dropping those brackets, and thus requiring `do` when you want a multi-form result, has been floated. I've warmed up to that idea, particularly since we've been dropping brackets from other stuff (like `import`) and `if` can't be used like `cond` anymore. I feel like in most cases, I won't need the `do`.

`if` and `when` are identical when you use exactly two arguments. What if `if` was required to have exactly three arguments? Then, when you see `if`, you know that an else-clause is coming. The expectation is that you'll use `when` when you're happy with the implicit `None` in the else-case, although you can always write `(if condition then-clause None)` if you want to be explicit.


Discussion:
There is precedent for this as Racket does enforce this distinction. Let me try it locally and see how it feels. 

Resolving patch:
diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py
--- a/hy/core/result_macros.py
+++ b/hy/core/result_macros.py
@@ -617,24 +617,23 @@ def compile_unpack_iterable(compiler, expr, root, arg):
 # ------------------------------------------------
 
 
-@pattern_macro("if", [FORM, FORM, maybe(FORM)])
+@pattern_macro("if", [FORM, FORM, FORM])
 def compile_if(compiler, expr, _, cond, body, orel_expr):
     cond = compiler.compile(cond)
     body = compiler.compile(body)
 
     nested = root = False
     orel = Result()
-    if orel_expr is not None:
-        if (
-            isinstance(orel_expr, Expression)
-            and isinstance(orel_expr[0], Symbol)
-            and orel_expr[0] == Symbol("if*")
-        ):
-            # Nested ifs: don't waste temporaries
-            root = compiler.temp_if is None
-            nested = True
-            compiler.temp_if = compiler.temp_if or compiler.get_anon_var()
-        orel = compiler.compile(orel_expr)
+    if (
+        isinstance(orel_expr, Expression)
+        and isinstance(orel_expr[0], Symbol)
+        and orel_expr[0] == Symbol("if*")
+    ):
+        # Nested ifs: don't waste temporaries
+        root = compiler.temp_if is None
+        nested = True
+        compiler.temp_if = compiler.temp_if or compiler.get_anon_var()
+    orel = compiler.compile(orel_expr)
 
     if not cond.stmts and isinstance(cond.force_expr, ast.Name):
         name = cond.force_expr.id
@@ -1810,6 +1809,7 @@ def compile_assert_expression(compiler, expr, root, test, msg):
             mkexpr(
                 "do", mkexpr("setv", msg_var, [msg]), mkexpr("assert", "False", msg_var)
             ),
+            Symbol("None"),
         ).replace(expr)
     )
 


Test patch:
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -73,7 +73,7 @@ def test_ast_bad_if():
 
 def test_ast_valid_if():
     "Make sure AST can compile valid if"
-    can_compile("(if foo bar)")
+    can_compile("(if foo bar baz)")
 
 
 def test_ast_valid_unary_op():
diff --git a/tests/native_tests/comprehensions.hy b/tests/native_tests/comprehensions.hy
--- a/tests/native_tests/comprehensions.hy
+++ b/tests/native_tests/comprehensions.hy
@@ -68,9 +68,9 @@
         (, 1 3 (, (, 1 0) (, 1 1) (, 1 2) (, 1 3)) 7)
         (, 1 3 (, (, 1 0) (, 1 1) (, 1 2) (, 1 3)) 9)]]
 
-    ['(f x (range 4) :do (if (not (% x 2)) (continue)) (* x 2))
+    ['(f x (range 4) :do (when (not (% x 2)) (continue)) (* x 2))
       [2 6]]
-    ['(f x (range 4) :setv p 9 :do (if (not (% x 2)) (continue)) (* x 2))
+    ['(f x (range 4) :setv p 9 :do (when (not (% x 2)) (continue)) (* x 2))
       [2 6]]
     ['(f x (range 20) :do (when (= x 3) (break)) (* x 2))
       [0 2 4]]
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -366,12 +366,7 @@
 
 
 (defn test-if []
-  ;; with an odd number of args, the last argument is the default case
-  (assert (= 1 (if 0 -1
-                     1)))
-  ;; with an even number of args, the default is None
-  (assert (is None (if 0 1))))
-
+  (assert (= 1 (if 0 -1 1))))
 
 (defn test-index []
   (assert (= (get {"one" "two"} "one") "two"))
@@ -1389,14 +1384,14 @@ cee\"} dee" "ey bee\ncee dee"))
 
 
 (defn test-break-breaking []
-  (defn holy-grail [] (for [x (range 10)] (if (= x 5) (break))) x)
+  (defn holy-grail [] (for [x (range 10)] (when (= x 5) (break))) x)
   (assert (= (holy-grail) 5)))
 
 
 (defn test-continue-continuation []
   (setv y [])
   (for [x (range 10)]
-    (if (!= x 5)
+    (when (!= x 5)
       (continue))
     (.append y x))
   (assert (= y [5])))
@@ -1634,7 +1629,7 @@ cee\"} dee" "ey bee\ncee dee"))
       (setv self.member-names []))
 
     (defn __setitem__ [self key value]
-      (if (not-in key self)
+      (when (not-in key self)
           (.append self.member-names key))
       (dict.__setitem__ self key value)))
 
diff --git a/tests/native_tests/let.hy b/tests/native_tests/let.hy
--- a/tests/native_tests/let.hy
+++ b/tests/native_tests/let.hy
@@ -191,7 +191,7 @@
 (defn test-let-break []
   (for [x (range 3)]
     (let [done (% x 2)]
-      (if done (break))))
+      (when done (break))))
   (assert (= x 1)))
 
 
@@ -199,7 +199,7 @@
   (let [foo []]
     (for [x (range 10)]
       (let [odd (% x 2)]
-        (if odd (continue))
+        (when odd (continue))
         (.append foo x)))
     (assert (= foo [0 2 4 6 8]))))
 
diff --git a/tests/native_tests/model_patterns.hy b/tests/native_tests/model_patterns.hy
--- a/tests/native_tests/model_patterns.hy
+++ b/tests/native_tests/model_patterns.hy
@@ -37,7 +37,7 @@
       (many FORM)])
     args))
   (defn f [loopers]
-    (setv head (if loopers (get loopers 0)))
+    (setv head (if loopers (get loopers 0) None))
     (setv tail (cut loopers 1 None))
     (print head)
     (cond
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -13,12 +13,13 @@
   (setv defns [])
   (for [o (if (isinstance op hy.models.Symbol) [op] op)]
     (defn replace [x]
-      (if (= x 'f)
-        o
-      (if (and (isinstance x Iterable) (not (isinstance x (, str bytes))))
-        ((type x) (map replace x))
-      (if True
-        x))))
+      (cond
+        (= x 'f)
+          o
+        (and (isinstance x Iterable) (not (isinstance x (, str bytes))))
+          ((type x) (map replace x))
+        True
+          x))
     (.append defns `(defn ~(hy.models.Symbol (+ "test_operator_" o "_real")) []
       (setv f-name ~(hy.models.String o))
       ~@(replace body)))
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -148,7 +148,7 @@ def test_bin_hy_stdin_except_do():
     assert "ccc" in output
 
     output, _ = run_cmd("hy",
-        '(if True (do "xxx" "yyy" "zzz"))')
+        '(when True "xxx" "yyy" "zzz")')
     assert "xxx" not in output
     assert "yyy" not in output
     assert "zzz" in output