Repository: hylang/hy

Issue:
Consider extending Hy's `.` form to accept calls
Clojure's `..` can have method calls. This is especially useful when chaining accessors, [for example](http://clojure.org/reference/java_interop#_the_dot_special_form):

``` Clojure
 (.. System (getProperties) (get "os.name"))
```

expands to `.`, which can also have calls:

``` Clojure
(. (. System (getProperties)) (get "os.name"))
```

This doesn't appear to work in Hy:

``` Hy
=> (import os)
=> (. os (getcwd))
  File "<input>", line 1, column 7

  (. os (getcwd))
        ^------^
HyTypeError: b'The attribute access DSL only accepts HySymbols and one-item lists, got HyExpression instead'
```

The error message seems to indicate that this should work, is this a bug? And why restrict it to one item? Clojure doesn't.

This makes chaining of accessors awkward (--spy):

``` Hy
=> (. ((. ((. os getcwd)) isalpha)) __class__ __name__)
os.getcwd().isalpha().__class__.__name__
'bool'
```

Although the `->` macro helps.

``` Hy
=> (-> os (.getcwd) (.isalpha) (. __class__) (. __name__))
os.getcwd().isalpha().__class__.__name__
'bool'
```

But Hy's `.` _can_ chain when not using calls

``` Hy
=> (. "" __class__ __name__)
''.__class__.__name__
'str'
```

This makes it more like Clojure's `..`, than Clojure's `.`, which can only take two arguments.

The proposed extended form would look like this: 

``` Hy
=> (. os (getcwd) (isalpha) __class__ __name__ [0])
os.getcwd().isalpha().__class__.__name__[0]
'b'
```

 The calls could also accept arguments, as Clojure.



Discussion:
The compiler could be simplified if we keep special forms as close as possible to the Python AST.

I think we should simplify the `.` special form to just do a single dot access, then make a core `..` macro built on top of that that can chain access, including with `[]` and `()`. This is also more consistent with Clojure.
I just want to bump this with a few other suggestions (if it were up to me, I'd just extend `.` but `..` is a good idea as well).

My suggestions are to just treat string and integer entries in `..` as implicitly inside `[]` so that they work as access:

    `(.. x 0 "y")`

Would be the same as 

    `(.. x [0] ["y"])`

I have a macro that does this (not super cleanly) already if someone wants to play with it. It also expands method calls as originally suggested.

```
(defmacro _. [&rest forms]
  (defn same-class-as [item exemplar]
    (= (type item)
       (type exemplar)))
  (defn expression? [item]
    (same-class-as item '(a)))
  (defn list? [item]
    (same-class-as item '[a]))
  (defn string-exp? [item]
    (same-class-as item '""))
  (defn symbol-exp? [item]
    (same-class-as item 'x))
  (defn int-exp? [item]
    (same-class-as item '10))
  (cond
    [(= 1 (len forms)) (first forms)]
    [True
     (setv head (first forms))
     (setv first-index (second forms))
     (setv rest-forms (cut forms 2))
     (cond
       [(expression? first-index)
        (setv method (first first-index))
        (setv args (cut first-index 1))
        `(_. ((. ~head ~method) ~@args)
             ~@rest-forms)]
       [(symbol-exp? first-index)
        `(_. (. ~head ~first-index) ~@rest-forms)]
       [(or (string-exp? first-index)
            (int-exp? first-index))
        `(_. (. ~head [~first-index]) ~@rest-forms)]
       [True `(_. (. ~head ~first-index) ~@rest-forms)])]))
```
this seems reasonable and pretty easy to do by changing the definition of the `.` macro to 
```diff
-@pattern_macro(".", [FORM, many(SYM | brackets(FORM))])
+@pattern_macro(".", [FORM, many(FORM)])
 def compile_attribute_access(compiler, expr, name, invocant, keys):
    ret = compiler.compile(invocant)

    for attr in keys:
        if isinstance(attr, Symbol):
            ret += asty.Attribute(attr,
                                  value=ret.force_expr,
                                  attr=mangle(attr),
                                  ctx=ast.Load())

-        else: # attr is a hy List
+        elif isinstance(attr, List):
             compiled_attr = compiler.compile(attr[0])
             ret = compiled_attr + ret + asty.Subscript(
                 attr,
                 value=ret.force_expr,
                 slice=ast.Index(value=compiled_attr.force_expr),
                 ctx=ast.Load())
+        elif isinstance(attr, Expression):
+            root, *args = attr
+            func = asty.Attribute(
+                root, value=ret.force_expr, attr=mangle(root), ctx=ast.Load()
+            )
+
+            args, funcret, keywords = compiler._compile_collect(args, with_kwargs=True)
+            ret += (
+                funcret
+                + func
+                + asty.Call(expr, func=func, args=args, keywords=keywords)
+            )
+        else:
+            raise ValueError("Bad member access")
 
     return ret
```
in my tests this seems to work in all the ways i expect it too. Does anyone see any reason this shouldn't be PR'd?

I would extend the pattern, rather than generalizing it to `FORM` and then raising an error in an if-else ladder in the body of the macro. However, I'm not entirely clear what the intended semantics are, so some tests would be helpful.

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
@@ -440,23 +440,56 @@ def compile_index_expression(compiler, expr, name, obj, indices):
 
     return ret
 
-@pattern_macro(".", [FORM, many(SYM | brackets(FORM))])
+
+
+notsym = lambda *dissallowed: some(
+    lambda x: isinstance(x, Symbol) and str(x) not in dissallowed
+)
+
+
+@pattern_macro(
+    ".",
+    [
+        FORM,
+        many(
+            SYM
+            | brackets(FORM)
+            | pexpr(notsym("unpack-iterable", "unpack-mapping"), many(FORM))
+        ),
+    ],
+)
 def compile_attribute_access(compiler, expr, name, invocant, keys):
     ret = compiler.compile(invocant)
 
     for attr in keys:
         if isinstance(attr, Symbol):
-            ret += asty.Attribute(attr,
-                                  value=ret.force_expr,
-                                  attr=mangle(attr),
-                                  ctx=ast.Load())
-        else: # attr is a hy List
+            ret += asty.Attribute(
+                attr, value=ret.force_expr, attr=mangle(attr), ctx=ast.Load()
+            )
+        elif isinstance(attr, Expression):
+            root, args = attr
+            func = asty.Attribute(
+                root, value=ret.force_expr, attr=mangle(root), ctx=ast.Load()
+            )
+
+            args, funcret, keywords = compiler._compile_collect(args, with_kwargs=True)
+            ret += (
+                funcret
+                + func
+                + asty.Call(expr, func=func, args=args, keywords=keywords)
+            )
+        else:  # attr is a hy List
             compiled_attr = compiler.compile(attr[0])
-            ret = compiled_attr + ret + asty.Subscript(
-                attr,
-                value=ret.force_expr,
-                slice=ast.Index(value=compiled_attr.force_expr),
-                ctx=ast.Load())
+            ret = (
+                compiled_attr
+                + ret
+                + asty.Subscript(
+                    attr,
+                    value=ret.force_expr,
+                    slice=ast.Index(value=compiled_attr.force_expr),
+                    ctx=ast.Load(),
+                )
+            )
 
     return ret
 
@@ -820,9 +853,6 @@ def compile_with_expression(compiler, expr, root, args, body):
 # * `switch`
 # ------------------------------------------------
 
-notsym = lambda *dissallowed: some(
-    lambda x: isinstance(x, Symbol) and str(x) not in dissallowed
-)
 keepsym = lambda wanted: some(lambda x: x == Symbol(wanted))
 _pattern = forward_decl()
 _pattern.define(


Test patch:
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
@@ -4,6 +4,7 @@
 
 (import tests.resources [kwtest function-with-a-dash AsyncWithTest]
         os.path [exists isdir isfile]
+        os
         sys :as systest
         re
         operator [or_]
@@ -1528,6 +1529,9 @@ cee\"} dee" "ey bee\ncee dee"))
   (assert (is (. foo [(+ 1 1)] __class__) mycls))
   (assert (= (. foo [(+ 1 1)] __class__ __name__ [0]) "m"))
   (assert (= (. foo [(+ 1 1)] __class__ __name__ [1]) "y"))
+  (assert (= (. os (getcwd) (isalpha) __class__ __name__ [0]) "b"))
+  (assert (= (. "ab hello" (strip "ab ") (upper)) "HELLO"))
+  (assert (= (. "hElLO\twoRld" (expandtabs :tabsize 4) (lower)) "hello   world"))
 
   (setv bar (mycls))
   (setv (. foo [1]) bar)