Repository: hylang/hy

Issue:
lambda vs fn
One of the subissues from #240.

They're not quite the same, but still too similar to need both. Hy's `lambda` will use a Python `lambda` if it can. When it can't, because there's a statement or multiple lines, it acts exactly the same as `fn`. I don't know about Hy, but Python mostly doesn't care to distinguish lambdas from ordinary function objects. You can even set `lambda`s as methods for classes.

I'm in favor of keeping `lambda` and removing `fn`. If there actually is a case where you need a one-expression function and can't use a Python lambda, then you can force a Hy lambda to be a function simply by adding a docstring.

If we're OK with not reserving Python keywords as discussed in #899, then after dropping `fn` we can rename `lambda` to `fn` to keep the shorter alias. Then Hy would treat `lambda` like any other symbol. This may be problematic for Python interop, but there are workarounds.



Discussion:
Alternative proposal:
**Make lambda different enough from fn to be worth another keyword**

Just filling up a keyword quota is not good language design. But I already had a good suggestion for this for #739: Require explicit `(return ...)` expressions in `fn`, but use automatic tail returns in `lambda`. This is how Python does it anyway, so it's not confusing. To be clear, multiline lambdas would still have to translate to "anonymous" Python functions, in which case the `return` is inserted automatically at the tails, as now.

We would also add a `deflambda` form as `defn` would now require explicit `(return ...)`. This is actually advantageous for class definitions, because we would no longer need to remember to put `nil`s in every tail position for `defn --init--` and `defn --new--`. Python automatically returns `None` if you don't return anything else.

This would also probably make #876 easier to implement, or at least make py2hy translations easier to do by hand.

If `deflambda` is too long, we could just call it `def`. `setv` already does the same thing anyway, so this will clean up another redundant keyword.


Resolving patch:
diff --git a/hy/cmdline.py b/hy/cmdline.py
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -164,11 +164,11 @@ def ideas_macro():
 
 
 ;;; filtering a list w/ a lambda
-(filter (lambda [x] (= (% x 2) 0)) (range 0 10))
+(filter (fn [x] (= (% x 2) 0)) (range 0 10))
 
 
 ;;; swaggin' functional bits (Python rulez)
-(max (map (lambda [x] (len x)) ["hi" "my" "name" "is" "paul"]))
+(max (map (fn [x] (len x)) ["hi" "my" "name" "is" "paul"]))
 
 """)])
 
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1417,12 +1417,28 @@ def compile_assoc_expression(self, expr):
     def compile_decorate_expression(self, expr):
         expr.pop(0)  # with-decorator
         fn = self.compile(expr.pop(-1))
-        if not fn.stmts or not (isinstance(fn.stmts[-1], ast.FunctionDef) or
-                                isinstance(fn.stmts[-1], ast.ClassDef)):
+        if fn.stmts and isinstance(fn.stmts[-1], (ast.FunctionDef,
+                                                  ast.ClassDef)):
+            decorators, ret, _ = self._compile_collect(expr)
+            fn.stmts[-1].decorator_list = (decorators +
+                                           fn.stmts[-1].decorator_list)
+            return ret + fn
+        elif fn.stmts and isinstance(fn.stmts[-1], ast.Assign):
+            # E.g., (with-decorator foo (setv f (fn [] 5)))
+            # We can't use Python's decorator syntax, but we can get the
+            # same effect.
+            decorators, ret, _ = self._compile_collect(expr)
+            for d in decorators:
+                fn.stmts[-1].value = ast.Call(func=d,
+                                              args=[fn.stmts[-1].value],
+                                              keywords=[],
+                                              starargs=None,
+                                              kwargs=None,
+                                              lineno=expr.start_line,
+                                              col_offset=expr.start_column)
+            return fn
+        else:
             raise HyTypeError(expr, "Decorated a non-function")
-        decorators, ret, _ = self._compile_collect(expr)
-        fn.stmts[-1].decorator_list = decorators + fn.stmts[-1].decorator_list
-        return ret + fn
 
     @builds("with*")
     @checkargs(min=2)
@@ -2285,17 +2301,15 @@ def compile_set(self, expression):
                             col_offset=expression.start_column)
         return ret
 
-    @builds("lambda")
     @builds("fn")
     @checkargs(min=1)
     def compile_function_def(self, expression):
-        called_as = expression.pop(0)
+        expression.pop(0)
 
         arglist = expression.pop(0)
         if not isinstance(arglist, HyList):
             raise HyTypeError(expression,
-                              "First argument to `{}' must be a list".format(
-                                  called_as))
+                              "First argument to `fn' must be a list")
 
         (ret, args, defaults, stararg,
          kwonlyargs, kwonlydefaults, kwargs) = self._parse_lambda_list(arglist)
@@ -2367,7 +2381,7 @@ def compile_function_def(self, expression):
             defaults=defaults)
 
         body = self._compile_branch(expression)
-        if not body.stmts and called_as == "lambda":
+        if not body.stmts:
             ret += ast.Lambda(
                 lineno=expression.start_line,
                 col_offset=expression.start_column,
@@ -2513,7 +2527,6 @@ def compile_macro(self, expression):
             if kw in expression[0]:
                 raise HyTypeError(name, "macros cannot use %s" % kw)
         new_expression = HyExpression([
-            HySymbol("with_decorator"),
             HyExpression([HySymbol("hy.macros.macro"), name]),
             HyExpression([HySymbol("fn")] + expression),
         ]).replace(expression)
@@ -2536,7 +2549,6 @@ def compile_reader(self, expression):
                                "for reader macro name" % type(name).__name__))
         name = HyString(name).replace(name)
         new_expression = HyExpression([
-            HySymbol("with_decorator"),
             HyExpression([HySymbol("hy.macros.reader"), name]),
             HyExpression([HySymbol("fn")] + expression),
         ]).replace(expression)


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
@@ -225,13 +225,13 @@ def test_ast_bad_defclass():
 
 def test_ast_good_lambda():
     "Make sure AST can compile valid lambda"
-    can_compile("(lambda [])")
-    can_compile("(lambda [] 1)")
+    can_compile("(fn [])")
+    can_compile("(fn [] 1)")
 
 
 def test_ast_bad_lambda():
     "Make sure AST can't compile invalid lambda"
-    cant_compile("(lambda)")
+    cant_compile("(fn)")
 
 
 def test_ast_good_yield():
@@ -369,9 +369,11 @@ def test_ast_expression_basics():
 
 def test_ast_anon_fns_basics():
     """ Ensure anon fns work. """
-    code = can_compile("(fn (x) (* x x))").body[0]
+    code = can_compile("(fn (x) (* x x))").body[0].value
+    assert type(code) == ast.Lambda
+    code = can_compile("(fn (x) (print \"multiform\") (* x x))").body[0]
     assert type(code) == ast.FunctionDef
-    code = can_compile("(fn (x))").body[0]
+    can_compile("(fn (x))")
     cant_compile("(fn)")
 
 
@@ -430,7 +432,7 @@ def test_lambda_list_keywords_kwargs():
 def test_lambda_list_keywords_kwonly():
     """Ensure we can compile functions with &kwonly if we're on Python
     3, or fail with an informative message on Python 2."""
-    kwonly_demo = "(fn [&kwonly a [b 2]] (print a b))"
+    kwonly_demo = "(fn [&kwonly a [b 2]] (print 1) (print a b))"
     if PY3:
         code = can_compile(kwonly_demo)
         for i, kwonlyarg_name in enumerate(('a', 'b')):
diff --git a/tests/compilers/test_compiler.py b/tests/compilers/test_compiler.py
--- a/tests/compilers/test_compiler.py
+++ b/tests/compilers/test_compiler.py
@@ -57,24 +57,6 @@ def _make_expression(*args):
     def setUp(self):
         self.c = compiler.HyASTCompiler('test')
 
-    def test_fn_compiler_empty_function(self):
-        ret = self.c.compile_function_def(
-            self._make_expression(HySymbol("fn"), HyList()))
-        self.assertEqual(ret.imports, {})
-
-        self.assertEqual(len(ret.stmts), 1)
-        stmt = ret.stmts[0]
-        self.assertIsInstance(stmt, ast.FunctionDef)
-        self.assertIsInstance(stmt.args, ast.arguments)
-        self.assertEqual(stmt.args.vararg, None)
-        self.assertEqual(stmt.args.kwarg, None)
-        self.assertEqual(stmt.args.defaults, [])
-        self.assertEqual(stmt.decorator_list, [])
-        self.assertEqual(len(stmt.body), 1)
-        self.assertIsInstance(stmt.body[0], ast.Pass)
-
-        self.assertIsInstance(ret.expr, ast.Name)
-
     def test_compiler_bare_names(self):
         """
         Check that the compiler doesn't drop bare names from code branches
diff --git a/tests/importer/test_importer.py b/tests/importer/test_importer.py
--- a/tests/importer/test_importer.py
+++ b/tests/importer/test_importer.py
@@ -12,7 +12,8 @@ def test_basics():
 
 def test_stringer():
     "Make sure the basics of the importer work"
-    _ast = import_buffer_to_ast("(defn square [x] (* x x))", '')
+    _ast = import_buffer_to_ast(
+        "(defn square [x] (print \"hello\") (* x x))", '')
     assert type(_ast.body[0]) == ast.FunctionDef
 
 
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
@@ -75,7 +75,7 @@
        (except [e [TypeError]] (assert (in "Can't assign to a builtin" (str e)))))
   (try (eval '(defn get [] (print "hello")))
        (except [e [TypeError]] (assert (in "Can't assign to a builtin" (str e)))))
-  (try (eval '(defn lambda [] (print "hello")))
+  (try (eval '(defn fn [] (print "hello")))
        (except [e [TypeError]] (assert (in "Can't assign to a builtin" (str e))))))
 
 
@@ -133,10 +133,6 @@
 
 (defn test-alias-names-in-errors []
   "NATIVE: tests that native aliases show the correct names in errors"
-  (try (eval '(lambda))
-       (except [e [Exception]] (assert (in "lambda" (str e)))))
-  (try (eval '(fn))
-       (except [e [Exception]] (assert (in "fn" (str e)))))
   (try (eval '(setv 1 2 3))
        (except [e [Exception]] (assert (in "setv" (str e)))))
   (try (eval '(def 1 2 3))
@@ -343,11 +339,11 @@
              "level")))
 
 
-(defn test-lambda []
-  "NATIVE: test lambda operator"
-  (setv square (lambda [x] (* x x)))
+(defn test-fn []
+  "NATIVE: test fn operator"
+  (setv square (fn [x] (* x x)))
   (assert (= 4 (square 2)))
-  (setv lambda_list (lambda [test &rest args] (, test args)))
+  (setv lambda_list (fn [test &rest args] (, test args)))
   (assert (= (, 1 (, 2 3)) (lambda_list 1 2 3))))
 
 
@@ -1424,7 +1420,7 @@
 (defmacro identify-keywords [&rest elts]
   `(list
     (map
-     (lambda (x) (if (is-keyword x) "keyword" "other"))
+     (fn (x) (if (is-keyword x) "keyword" "other"))
      ~elts)))
 
 (defn test-keywords-and-macros []
diff --git a/tests/native_tests/with_decorator.hy b/tests/native_tests/with_decorator.hy
--- a/tests/native_tests/with_decorator.hy
+++ b/tests/native_tests/with_decorator.hy
@@ -1,19 +1,40 @@
-(defn foodec [func]
-  (lambda [] (+ 1 1)))
+(defn test-decorated-1line-function []
+  (defn foodec [func]
+    (fn [] (+ (func) 1)))
+  (with-decorator foodec
+    (defn tfunction []
+      (* 2 2)))
+  (assert (= (tfunction) 5)))
 
 
-(with-decorator foodec
-  (defn tfunction []
-    (* 2 2)))
+(defn test-decorated-multiline-function []
+  (defn bazdec [func]
+    (fn [] (+ (func) "x")))
+  (with-decorator bazdec
+    (defn f []
+      (setv intermediate "i")
+      (+ intermediate "b")))
+  (assert (= (f) "ibx")))
 
 
-(defn bardec [cls]
-  (setv cls.my_attr 123)
-  cls)
+(defn test-decorated-class []
+  (defn bardec [cls]
+    (setv cls.attr2 456)
+    cls)
+  (with-decorator bardec
+    (defclass cls []
+      [attr1 123]))
+  (assert (= cls.attr1 123))
+  (assert (= cls.attr2 456)))
+
+
+(defn test-decorated-setv []
+  (defn d [func]
+    (fn [] (+ (func) "z")))
+  (with-decorator d
+    (setv f (fn [] "hello")))
+  (assert (= (f) "helloz")))
 
-(with-decorator bardec
-  (defclass cls []
-    [my_attr 456]))
 
 (defn test-decorator-clobbing []
   "NATIVE: Tests whether nested decorators work"
@@ -24,8 +45,3 @@
       (with-decorator dec2
         (defn f [] 1)))
     (assert (= (f) 4))))
-
-(defn test-decorators []
-  "NATIVE: test decorators."
-  (assert (= (tfunction) 2))
-  (assert (= cls.my_attr 123)))