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

assoc for attrs? #35

Open
gilch opened this issue Mar 14, 2018 · 8 comments
Open

assoc for attrs? #35

gilch opened this issue Mar 14, 2018 · 8 comments
Labels
feature New feature or request

Comments

@gilch
Copy link
Member

gilch commented Mar 14, 2018

I'm thinking aloud here and looking for feedback.

This is a gap in Hy that's bothered me for a while, since I saw https://stackoverflow.com/questions/28949486/is-there-a-way-in-the-hy-language-to-use-doto-on-self

The advantage assoc has over setv is that it avoids repeating the location reference when setting multiple pairs e.g.

(setv 
  (get spam "foo") 1
  (get spam "bar") 2
  (get spam "baz") 3)

vs

(assoc spam
  "foo" 1
  "bar" 2
  "baz" 3)

It's much better than setv/get, though this is only slightly better than

(.update spam
         {"foo" 1
          "bar" 2
          "baz" 3})

it doesn't rely on the .update method being present and saves some brackets. Nice, but not essential?

But a much more common case in Python, especially when using classes, is multiple assignments to different attrs of the same object.

(defclass Point3d []
  (defn __init__[self x y z]
    (setv
      (. self x) x
      (. self y) y
      (. self z) z)))

It's the same kind of repetition. The self.x sugar helps a little, but not when the object reference is itself an expression.

But we don't have a convenient .update method, so the use case is even stronger. You could access the backing dict and do something like (.update (vars self) (vars)), but that feels a little dirty. It gives you a self.self for one thing (and any other local you've set, including hy_anon stuff and gensysms). And you can't specify different names for the attr than the parameter. We can do better:

(.update (vars self)
         {'x x  'y y  'z z})

And even better with assoc

(assoc (vars self)
  'x x  'y y  'z z))

But we still have to call vars and we still have to quote the attr names. This is probably the best we can do without introducing a new macro.

You might think you could use doto for updating an object, but it's not that easy:

(doto self
    (-> (. x) (setv x))
    (-> (. y) (setv y))
    (-> (. z) (setv z)))

doto really only works for method calls. This really isn't any easier to type. Do we want to force people to write accessors so they can use doto? (Or provide a macro to write them?) I think not, for better interop with existing Python modules. Python is not Java. doto was good enough for Clojure, when you always expect accessors. But you just can't expect that in Python.

But if we had some kind of an assoc for attrs, it would just be--

(assoc-for-attrs self
  x x   y y   z z)

Easy. We'd want a shorter name than assoc-for-attrs though.

Is there some easier way to do this I'm not seeing? Is assoc worth keeping in the first place over .update? Just to avoid some quotes and brackets? And if so, why don't we have the arguably more important assoc-for-attrs?

But maybe we could do even better than that. Parameter names often are the same as the attr names. You shouldn't have to say it twice if the names are the same. We could have a local-to-attr form,

(local-to-attr self x y z)
;; same as (setv self.x x  self.y y  self.z z)

But again, maybe with a shorter name. We could even combine the two somehow, but it would complicate the macro. Here's one possibility

(insert-better-name self .x .y  my_foo a_foo)
;; same as (setv self.x x  self.y y  self.my_foo a_foo)

Better ideas?

@tuturto
Copy link

tuturto commented Mar 14, 2018

Setting attributes of object from parameters in --init-- is so common that I ended up with small macro just for that (excuse the naming):

(defmacro set-attributes [&rest attributes]
  "set attributes of object with respective parameters in --init--"
  `(do ~@(genexpr `(setv (. self ~x) ~x) [x attributes])))

Arguably it could be called to init-attrs or even assoc-a, but I never got around it. It's also specific to setting values to self, but that could be fixed by introducing a new parameter. Back when I was thinking about this I couldn't come up with a better way to handle the situation.

In any case, I would keep assoc around for future, if for nothing else than for saving couple characters while typing.

@Kodiologist
Copy link
Member

In Rogue TV, I wrote a macro set-self that does the same thing as Tuuka's set-attributes. The fact that we independently came up with this macro is a hint that it would be a useful addition to Hy core. But maybe a more general form like Matthew's local-to-attr or insert-better-name is a better idea.

hylang/hy#1119 is also related.

@vodik
Copy link

vodik commented Mar 14, 2018

Also thinking aloud, Maybe we could also look at borrowing something from attrs package? https://github.com/python-attrs/attrs

Considering its partially made it into Python proper with PEP 557, might not be a bad way to do stuff.

@gilch
Copy link
Member Author

gilch commented Apr 29, 2018

Here's an improved insert-better-name. Call it attach (it starts with att like attr)

(attach self x y :my_foo a_foo)
;; same as (setv self.x x  self.y y  self.my_foo a_foo)

The first argument gets the attachments. The positional arguments come from locals of the same name, and the kwargs let you change the name.

@gilch
Copy link
Member Author

gilch commented Apr 30, 2018

Here's an implementation.

(defmacro! attach [o!target &rest vs]
  (setv ivs (iter vs))
  (import hy)
  `(setv ~@(chain.from-iterable (genexpr [`(. ~g!target ~(if (instance? hy.HyKeyword arg)
                                                             (hy.HySymbol (cut (str arg) 1))
                                                             arg))
                                          (if (instance? hy.HyKeyword arg)
                                              (next ivs)
                                              arg)]
                                         [arg ivs]))))

I feel like it shouldn't be this hard. I feel like the hy module is important enough to autoimport, but I don't want autoimports at all. hylang/hy#1407.

The models should probably be called things like hy.Keyword instead of hy.HyKeyword. Adding the module name as a prefix seems redundant.

HyKeyword's __str__ shouldn't include the colon so I don't have to cut it off like that, since converting it to a symbol isn't that unusual in macros. In fact, maybe keywords should just have a .to-symbol method for that. (That or HySymbol should accept a HyKeyword instance in its constructor, or have a static factory method that does.)

chain.from-iterable is too verbose. A chain #* might be a better alternative. But I see this (chain.from-iterable (genexpr pattern a lot. (It's like the list monad.) I had a chain-comp macro that combined them in my destructure module hylang/hy#1328. It might be worth having something like that in core too.

Example usage:

=> (setv a 1  b 2  c 3  spam (fn[]))
a = 1
b = 2
c = 3
spam = lambda : None
None

=> (attach spam a b :foo c)
_hyx_XsemicolonXtargetXvertical_lineX1235 = spam
None
None
_hyx_XsemicolonXtargetXvertical_lineX1235.a = a
_hyx_XsemicolonXtargetXvertical_lineX1235.b = b
_hyx_XsemicolonXtargetXvertical_lineX1235.foo = c
None

The gensyms aren't pretty hylang/hy#1531. We're also getting extra Nones for some reason hylang/hy#1424.

I feel like I'm re-implementing &kwargs a little bit. For maximum flexibility, I don't think defmacro should have that &kwargs hylang/hy#960, but maybe core could provide some function to help parse keyword arguments in macros. I'm not sure what that would look like without giving up generality though.

@Kodiologist
Copy link
Member

I feel like it shouldn't be this hard.

Model patterns (#1593) would help. ;)

HyKeyword's __str__ shouldn't include the colon so I don't have to cut it off like that

It's better to use (name kw) or kw.name than (cut (str kw) 1).

@gilch
Copy link
Member Author

gilch commented Apr 30, 2018

It's better to use (name kw)

Forgot about that one. Revision.

(defmacro! attach [o!target &rest vs]
  (setv ivs (iter vs))
  (import hy)
  `(setv ~@(chain #*(genexpr [`(. ~g!target ~(if (instance? hy.HyKeyword v)
                                                 (hy.HySymbol (name v))
                                                 v))
                              (if (instance? hy.HyKeyword v)
                                  (next ivs)
                                  v)]
                             [v ivs]))))

Still works.

Model patterns (#1593) would help. ;)

That does look promising. I've been looking through the funcparserlib docs. I will try to make time to review hylang/hy#1593, but have been busy with work lately. The associated small PRs may have to come first.

@Kodiologist Kodiologist transferred this issue from hylang/hy Mar 5, 2022
@Kodiologist
Copy link
Member

These days you can use meth to write __init__s. meth allows things like

(require hyrule [meth])

(defclass Point3d []
  (meth __init__ [x y z]
    (setv
      @x x
      @y y
      @z z)))

or, more concisely,

(require hyrule [meth])

(defclass Point3d []
  (meth __init__ [@x @y @z]))

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

No branches or pull requests

4 participants