Repository: hylang/hy

Issue:
Get rid of the __init__ return suppression automagic and replace with a definit macro
This kind of automagic makes me uncomfortable.  It's also complicating the compiler and seems to be the cause of hylang/hy#1533.

Python has a very dynamic runtime. You can rewrite existing classes on the fly because they're objects too. You can introducing new methods and delete old ones. Metaclasses may need to do this kind of rewriting internally to function.

But the `__init__` automagic to return `None` only works in a defclass form. Not if `__init__` was declared as a function outside and the attached later.

Worse, if some metaclass other than `type` requires an `__init__` method to actually return something, it may be impossible to use in Hy.

I'm also worried this may cause issues with decorators. Does the automagic even work if `__init__` is decorated? And even worse, what if **does** work, but the decorator requires a return value in the decorated method? It would be difficult to use that in Hy.

I was opposed to this automagic from the start, but we did add this for a reason. Usually automatic returns are more convenient in Hy. But that's an error in `__init__` so it's a pain to remember to always end `__init__` methods with `None` in Hy (if the last form happens to return `None`, as many do, you might not notice the problem right away).

Explicit is better than implicit. Let's get rid of the automagic and just use a simple core `definit` macro instead, like this,
```
(defmacro definit [argslist &rest body]
  `(defn __init__ ~argslist ~@body None))
```
Which you'd use like
```
(defclass Foo []
  (definit [self x]
    (setv self.x x))
```
It returns `None` as required by the standard `type` metaclass, and `definit` is easier to type, than `def __init__` so it'll get used, instead of forgotten.


Discussion:
I share your discomfort with `__init__` rewriting, at least to some degree, but I feel like it wouldn't be much easier to remember to use `definit` than to include the explicit `None`.

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1856,8 +1856,7 @@ def compile_class_expression(self, expr, root, name, rest):
             bodyr += self.compile(docstring).expr_as_stmt()
 
         for e in body:
-            e = self.compile(self._rewire_init(
-                macroexpand(e, self.module, self)))
+            e = self.compile(macroexpand(e, self.module, self))
             bodyr += e + e.expr_as_stmt()
 
         return bases + asty.ClassDef(
@@ -1870,42 +1869,6 @@ def compile_class_expression(self, expr, root, name, rest):
             bases=bases_expr,
             body=bodyr.stmts or [asty.Pass(expr)])
 
-    def _rewire_init(self, expr):
-        "append None to definitions of __init__."
-
-        if (
-            isinstance(expr, Expression)
-            and expr[0] == Symbol("defn")
-            and expr[1] == Symbol("__init__")
-        ):
-            return Expression([*expr, Symbol("None")]).replace(expr)
-        elif not (isinstance(expr, Expression)
-            and len(expr) > 1
-            and expr[0] == Symbol("setv")):
-            return expr
-        else:
-            new_args = []
-            decls = list(expr[1:])
-            while decls:
-                if is_annotate_expression(decls[0]):
-                    # Handle annotations.
-                    ann = decls.pop(0)
-                else:
-                    ann = None
-
-                if len(decls) < 2:
-                    break
-                k, v = (decls.pop(0), decls.pop(0))
-                if isinstance(k, Symbol) and mangle(k) == "__init__" and isinstance(v, Expression):
-                    v += Expression([Symbol("None")])
-
-                if ann is not None:
-                    new_args.append(ann)
-
-                new_args.extend((k, v))
-            return (Expression([Symbol("setv")] + new_args + decls)
-                .replace(expr))
-
     @special(["eval-and-compile", "eval-when-compile"], [many(FORM)])
     def compile_eval_and_compile(self, expr, root, body):
         new_expr = Expression([Symbol("do").replace(expr[0])]).replace(expr)


Test patch:
diff --git a/tests/native_tests/defclass.hy b/tests/native_tests/defclass.hy
--- a/tests/native_tests/defclass.hy
+++ b/tests/native_tests/defclass.hy
@@ -114,20 +114,6 @@
   (assert foo 2)
   (assert (.greet a) "hello"))
 
-(defn test-defclass-implicit-none-for-init []
-  "NATIVE: test that defclass adds an implicit None to __init__"
-  (defclass A []
-    (setv __init__ (fn [self] (setv self.x 1) 42)))
-  (defclass B []
-    (defn __init__ [self]
-      (setv self.x 2)
-      42))
-
-  (setv a (A))
-  (setv b (B))
-  (assert (= a.x 1))
-  (assert (= b.x 2)))
-
 (defn test-class-sideeffects []
   "NATIVE: test that defclass runs all expressions"
   (defn set-sentinel []