Repository: hylang/hy

Issue:
Remove redundant braces in defclass
For example, (from Hy's docs)
from this

```
=> (defclass Cat []
...  [[age None]
...   [colour "white"]
...   [speak (fn [self] (print "Meow"))]])
```

to this

```
=> (defclass Cat []
...   age None
...   colour "white"
...   speak (fn [self] (print "Meow")))
```



Discussion:
No strong opinion on this, but I'd keep one of the braces:

```
(defclass Cat []
   [age None
    colour "white"
    speak (fn [self] (print "Meow"))])
```

This would make it a tiny bit easier and more straightforward to add docstring support (if we want / can):

```
(defclass Cat []
   "Kitty kat says Meow"
   [age None
    ...])
```

```
(defclass Cat []
  --doc-- "Kitty kat says Meow"
  age None
...)
```

Just pointing out that the brackets are not required for docstrings. 

Which is better? I don't know. 

`--doc--` is five whole characters longer than the `[]` we just got rid of. It's arguably the more common case to have docstrings in classes than not. It's also more consistent with functions to have an implicit --doc-- instead of an explicit assignment.

On the other hand, 
    Explicit is better than implicit.
    Simple is better than complex.
    Flat is better than nested.
    Special cases aren't special enough to break the rules.
Once you start using macros to define classes anyway, isn't it just easier not to deal with the special cases?

I honestly hadn't thought of the docstrings when I made that simplification. I wonder if I'm missing anything else. Do class declarations in Python do anything else special (i.e. that isn't like an assignment)?

Yuck, `--doc--` looks terrible, sorry. We _could_ do without an explicit docstring marker, and without brackets, but that'd be even more confusing. Just a hunch, but I'm for only removing one level of brackets. But, as I said earlier, I have no strong arguments for or against either way.

Doesn't `--init--` look equally terrible? We're not getting around that one, are we? I don't think it looks that bad though.

I'm beginning to think that the entire `defclass` form is flawed. It's too different from the way Python does it to fit well.

``` Python
>>> # class definitions can have side effects
>>> class C:
    print('side effect!')


side effect!
>>> # can contain loops
>>> class C:
    for i in range(3):
        print(i)


0
1
2
>>> C.i
2
>>> # can clean up with del
>>> class C:
    for i in range(3):
        print(i)
    del i


0
1
2
>>> C.i
Traceback (most recent call last):
  File "<pyshell#16>", line 1, in <module>
    C.i
AttributeError: type object 'C' has no attribute 'i'
>>> # can refer to outer scope
>>> def foo():
    spam = 42
    class C:
        nonlocal spam
        spam = 24
    print(spam)


>>> foo()
24
>>> eggs = 2
>>> class C:
    global eggs
    eggs += 1
    print(eggs)


3
>>> eggs
3
>>> # can catch exceptions without failing
>>> 1/0
Traceback (most recent call last):
  File "<pyshell#44>", line 1, in <module>
    1/0
ZeroDivisionError: division by zero
>>> class C:
    try:
        1/0
    except ZeroDivisionError:
        pass
    msg = "still good!"


>>> C.msg
'still good!'
>>> # can build differently on conditions
>>> def makeC():
    class C:
        if raining:
            __umbrella = True
        else:
            __sunglasses = True
    return C

>>> raining = True
>>> dir(makeC())
['_C__umbrella', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> raining = False
>>> dir(makeC())
['_C__sunglasses', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> # can do internal imports
>>> class C:
    import re


>>> re
Traceback (most recent call last):
  File "<pyshell#72>", line 1, in <module>
    re
NameError: name 're' is not defined
>>> C.re
<module 're' from 'C:\\Python34\\lib\\re.py'>
>>> # can contain nested inner classes
>>> class C:
    class D:
        spam = 42


>>> C.D.spam
42
```

Basically, a lot of what we miss for want of proper `let`, we _could_ do with Python classes. But I can't do ANY of this with `defclass`!

We don't need special square bracket syntax for setting attributes. `setv` can do multiple pairs now. Just use one of those.

```
(defclass Cat []
  "docstring"
  (setv age None
        colour "white")
  (defn speak [self] 
    (print "meow"))
```

The last proposed syntax looks fine to me. A noticable improvement over the current one, :+1: for that!

Ping @hylang/core, any other opinions?

I was also toying with this idea:

``` hy
(defclass Cat []
  "docstring"
  [age None
   colour "white"]
  (for [x (range 10)]
    (print "side effect #" x))
  (defn speak [self]
    (print "meow"))
```

That is, we'd have an optional property list, because that looks nicer than `(setv ...)`. Parsing would go like this:
- Is the first arg a string? Yes? Docstring! If no, continue.
- Is the next arg a HyList? Yes? Property list! If no, continue.
- Everything else translates to stuff within the class's scope.

I enjoy the last example a lot. Should give enough flexibility while also supporting the python features

Another idea: have a `class` that doesn't do much sugar:

``` hy
(class Cat []
  "optional docstring"
  (setv ...)
  (defn ...))
```

And build a `(defclass)` macro on top of that.


Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -2238,15 +2238,15 @@ def compile_function_def(self, expression):
 
     @builds("defclass")
     @checkargs(min=1)
-    def compile_class_expression(self, expression):
-        expression.pop(0)  # class
+    def compile_class_expression(self, expressions):
+        expressions.pop(0)  # class
 
-        class_name = expression.pop(0)
+        class_name = expressions.pop(0)
 
-        if expression:
-            base_list = expression.pop(0)
+        if expressions:
+            base_list = expressions.pop(0)
             if not isinstance(base_list, HyList):
-                raise HyTypeError(expression,
+                raise HyTypeError(expressions,
                                   "Bases class must be a list")
             bases_expr, bases, _ = self._compile_collect(base_list)
         else:
@@ -2256,8 +2256,8 @@ def compile_class_expression(self, expression):
         body = Result()
 
         # grab the doc string, if there is one
-        if expression and isinstance(expression[0], HyString):
-            docstring = expression.pop(0)
+        if expressions and isinstance(expressions[0], HyString):
+            docstring = expressions.pop(0)
             symb = HySymbol("__doc__")
             symb.start_line = docstring.start_line
             symb.start_column = docstring.start_column
@@ -2266,31 +2266,25 @@ def compile_class_expression(self, expression):
                                          docstring.start_column)
             body += body.expr_as_stmt()
 
-        if expression:
-            try:
-                body_expression = iter(expression.pop(0))
-            except TypeError:
-                raise HyTypeError(
-                    expression,
-                    "Wrong argument type for defclass attributes definition.")
-            for b in body_expression:
-                if isinstance(b, HyExpression):
-                    b = macroexpand(b, self.module_name)
-                if len(b) != 2:
-                    raise HyTypeError(
-                        expression,
-                        "Wrong number of argument in defclass attribute.")
-                body += self._compile_assign(b[0], b[1],
-                                             b.start_line, b.start_column)
-                body += body.expr_as_stmt()
+        if expressions and isinstance(expressions[0], HyList) \
+           and not isinstance(expressions[0], HyExpression):
+            expr = expressions.pop(0)
+            body += self.compile(
+                HyExpression([
+                    HySymbol("setv")
+                ] + expr).replace(expr)
+            )
+
+        for expression in expressions:
+            body += self.compile(macroexpand(expression, self.module_name))
 
         if not body.stmts:
-            body += ast.Pass(lineno=expression.start_line,
-                             col_offset=expression.start_column)
+            body += ast.Pass(lineno=expressions.start_line,
+                             col_offset=expressions.start_column)
 
         return bases + ast.ClassDef(
-            lineno=expression.start_line,
-            col_offset=expression.start_column,
+            lineno=expressions.start_line,
+            col_offset=expressions.start_column,
             decorator_list=[],
             name=ast_str(class_name),
             keywords=[],


Test patch:
diff --git a/tests/native_tests/contrib/meth.hy b/tests/native_tests/contrib/meth.hy
--- a/tests/native_tests/contrib/meth.hy
+++ b/tests/native_tests/contrib/meth.hy
@@ -2,12 +2,11 @@
 
 (defclass FakeMeth []
   "Mocking decorator class"
-  [[rules {}]
-   [route (fn [self rule &kwargs options]
-            (fn [f]
-              (assoc self.rules rule (, f options))
-              f))]])
-
+  [rules {}]
+  (defn route [self rule &kwargs options]
+    (fn [f]
+      (assoc self.rules rule (, f options))
+      f)))
 
 (defn test_route []
   (let [[app (FakeMeth)]]
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
@@ -23,7 +23,7 @@
 (defn test-defclass-attrs []
   "NATIVE: test defclass attributes"
   (defclass A []
-    [[x 42]])
+    [x 42])
   (assert (= A.x 42))
   (assert (= (getattr (A) "x")  42)))
 
@@ -31,9 +31,9 @@
 (defn test-defclass-attrs-fn []
   "NATIVE: test defclass attributes with fn"
   (defclass B []
-    [[x 42]
-     [y (fn [self value]
-          (+ self.x value))]])
+    [x 42
+     y (fn [self value]
+         (+ self.x value))])
   (assert (= B.x 42))
   (assert (= (.y (B) 5) 47))
   (let [[b (B)]]
@@ -44,17 +44,17 @@
 (defn test-defclass-dynamic-inheritance []
   "NATIVE: test defclass with dynamic inheritance"
   (defclass A [((fn [] (if true list dict)))]
-    [[x 42]])
+    [x 42])
   (assert (isinstance (A) list))
   (defclass A [((fn [] (if false list dict)))]
-    [[x 42]])
+    [x 42])
   (assert (isinstance (A) dict)))
 
 
 (defn test-defclass-no-fn-leak []
   "NATIVE: test defclass attributes with fn"
   (defclass A []
-    [[x (fn [] 1)]])
+    [x (fn [] 1)])
   (try
    (do
     (x)
@@ -64,13 +64,13 @@
 (defn test-defclass-docstring []
   "NATIVE: test defclass docstring"
   (defclass A []
-    [[--doc-- "doc string"]
-     [x 1]])
+    [--doc-- "doc string"
+     x 1])
   (setv a (A))
   (assert (= a.__doc__ "doc string"))
   (defclass B []
     "doc string"
-    [[x 1]])
+    [x 1])
   (setv b (B))
   (assert (= b.x 1))
   (assert (= b.__doc__ "doc string"))
@@ -78,7 +78,7 @@
     "begin a very long multi-line string to make
      sure that it comes out the way we hope
      and can span 3 lines end."
-    [[x 1]])
+    [x 1])
   (setv mL (MultiLine))
   (assert (= mL.x 1))
   (assert (in "begin" mL.__doc__))
@@ -86,8 +86,26 @@
 
 (defn test-defclass-macroexpand []
   "NATIVE: test defclass with macro expand"
-  (defmacro M [] `[x (fn [self x] (setv self._x x))])
-  (defclass A [] [(M)])
+  (defmacro M [] `(defn x [self x] (setv self._x x)))
+  (defclass A [] (M))
   (setv a (A))
   (a.x 1)
   (assert (= a._x 1)))
+
+(defn test-defclass-syntax []
+  "NATIVE: test defclass syntax with properties and methods and side-effects"
+  (setv foo 1)
+  (defclass A []
+    [x 1
+     y 2]
+    (global foo)
+    (setv foo 2)
+    (defn greet [self]
+      "Greet the caller"
+
+      "hello!"))
+  (setv a (A))
+  (assert (= a.x 1))
+  (assert (= a.y 2))
+  (assert foo 2)
+  (assert (.greet a) "hello"))
diff --git a/tests/native_tests/mathematics.hy b/tests/native_tests/mathematics.hy
--- a/tests/native_tests/mathematics.hy
+++ b/tests/native_tests/mathematics.hy
@@ -145,21 +145,21 @@
 
 
 (defclass HyTestMatrix [list]
-  [[--matmul--
-    (fn [self other]
-      (let [[n (len self)]
-            [m (len (. other [0]))]
-            [result []]]
-        (for [i (range m)]
-          (let [[result-row []]]
-            (for [j (range n)]
-              (let [[dot-product 0]]
-                (for [k (range (len (. self [0])))]
-                  (+= dot-product (* (. self [i] [k])
-                                     (. other [k] [j]))))
-                (.append result-row dot-product)))
-            (.append result result-row)))
-        result))]])
+  [--matmul--
+   (fn [self other]
+     (let [[n (len self)]
+           [m (len (. other [0]))]
+           [result []]]
+       (for [i (range m)]
+         (let [[result-row []]]
+           (for [j (range n)]
+             (let [[dot-product 0]]
+               (for [k (range (len (. self [0])))]
+                 (+= dot-product (* (. self [i] [k])
+                                    (. other [k] [j]))))
+               (.append result-row dot-product)))
+           (.append result result-row)))
+       result))])
 
 (def first-test-matrix (HyTestMatrix [[1 2 3]
                                       [4 5 6]
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
@@ -13,7 +13,7 @@
 
 (with-decorator bardec
   (defclass cls []
-    [[my_attr 456]]))
+    [my_attr 456]))
 
 (defn test-decorator-clobbing []
   "NATIVE: Tests whether nested decorators work"
diff --git a/tests/native_tests/with_test.hy b/tests/native_tests/with_test.hy
--- a/tests/native_tests/with_test.hy
+++ b/tests/native_tests/with_test.hy
@@ -1,16 +1,13 @@
 (defclass WithTest [object]
-  [(--init-- 
-    (fn [self val]
-      (setv self.val val)
-      None))
+  (defn --init-- [self val]
+    (setv self.val val)
+    None)
 
-  (--enter--
-    (fn [self]
-      self.val))
+  (defn --enter-- [self]
+    self.val)
 
-  (--exit--
-    (fn [self type value traceback]
-      (setv self.val None)))])
+  (defn --exit-- [self type value traceback]
+    (setv self.val None)))
 
 (defn test-single-with []
   "NATIVE: test a single with"