Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider extending Hy's . form to accept calls #1108

Closed
gilch opened this issue Sep 19, 2016 · 4 comments · Fixed by #2128
Closed

Consider extending Hy's . form to accept calls #1108

gilch opened this issue Sep 19, 2016 · 4 comments · Fixed by #2128
Labels

Comments

@gilch
Copy link
Member

gilch commented Sep 19, 2016

Clojure's .. can have method calls. This is especially useful when chaining accessors, for example:

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

expands to ., which can also have calls:

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

This doesn't appear to work in 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):

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

Although the -> macro helps.

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

But Hy's . can chain when not using calls

=> (. "" __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:

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

The calls could also accept arguments, as Clojure.

@gilch
Copy link
Member Author

gilch commented Sep 2, 2017

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.

@VincentToups
Copy link

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)])]))

@allison-casey
Copy link
Contributor

allison-casey commented Jul 19, 2021

this seems reasonable and pretty easy to do by changing the definition of the . macro to

-@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?

@Kodiologist
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants