-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Meta attribute #3620
Comments
Nice feature. Though, |
I'm all in to be able to add attributes/metadata to ivars.
class JsonAttribute < Attribute
end
# will allow to use @[Json(*options)] Maybe 1.1. Maybe the allowed
2+1. Multiplicity can also be restricted in the declaration.
3+1. Again, could be restricted in the declaration if wanted and generate compile time errors.
class Customer
@[Apply(@name, Json, *options)]
# ...
end So basically a
|
I too would prefer a strongly typed, more powerful implementation, such as in C# or java. We should at the very least be able to implement flags enums, with the current syntax, with just standard library code. How about this for a syntax? @[Targets(:class)]
@[AllowMultiple]
attribute JSON(name : String, allow_nil)
# this is now macro code which receives the class and expands inside it
end I think that attributes being themselves macros is a powerful concept which is quite simple, and may work well. Obviously they should be used in moderation, but that's true for macros in general. Adding a This is a rough first idea I thought up in 20 minutes so please do criticize it. |
In terms of tagging getter/setter/property, the simplest way forward would just to have the property macro place the attributes it has on the instance variable. This would mean attributes expand after normal macros (if we make attributes functions), and that macros could receive the attributes placed on their call. |
Note that with my proposal, data only attributes would simply have an empty body. They would still be traversable meta data on instance variables. For example, the |
I like the feature a lot! I'm not fond of the I'd prefer to be able to specify many metas (or tags?) at once: @[Meta(:json, :yaml)]
property x : Int32 |
@bcardiff @RX14 In my proposal the implementation is very easy: when you ask for an attribute of a given name ( @[Meta(:json, {converter: Time::EpochConverter})]
@x : Time is very similar to doing: time: {type: Time, converter: Time::EpochConverter} We just changed the way this information is exposed and stored. However, adding an That's why I was aiming for something simple. In fact, this is very similar to how Go implements metadata: https://golang.org/pkg/reflect/#StructTag package main
import (
"fmt"
"reflect"
)
func main() {
type S struct {
F string `species:"gopher" color:"blue"`
}
s := S{}
st := reflect.TypeOf(s)
field := st.Field(0)
fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
}
That is, in Go a tag is just a string with some convention on it. No need to declare attributes, their type, etc. At runtime you just parse it and do whatever you need with it. In Crystal this would be similar (according to my proposal): you can attach any metadata (any AST node), and then access it only at compile-time and do whatever you need with it. This feels more lightweight and in fact much more powerful, because if we bring types in then we'll be more restricted (like in the converter example above). Also, it works in Go, and I like its simplicity, so I think it can work too for Crystal. But, having typed attributes is definitely an option. But if we want to consider it, someone (@bcardiff, @RX14) has to write a full proposal about all the details (class/struct declaration, constructor? methods? types?) Also... what is a use case where you would want multiple instances of the same attribute applied to an entity? |
Actually, we could have annotation definitions, but I would make them untyped. So maybe something like this: # This declares an attribute together with the required arguments (here empty),
# the optional arguments (key, nilable, etc.),
# also the target, maybe the multiplicity
@[Attribute(JSON,
required: [],
optional: [:key, :nilable, :default, :emit_null, :converter, :root]),
target: :instance_var] To use it: @[JSON]
@x : Int32
@[JSON(converter: Time::EpochConverter)]
@time : Time We can probably store attributes under a different namespace, so the I admit that this does look better than the original proposal, and one could also do now: @[JSON, YAML]
@x : Int32
@[JSON(key: "foo"), YAML(key: "foo")]
@y : Int32 This is just a bit less lightweight than the original proposal, but it looks a bit better (shorter to use annotations), we get a bit of type safety (the annotation name), and it's easy to apply multiple attributes to a variable. It still keeps the "metadata is an AST node" thing, though. But I think this is better, because this metadata will be processed by macros at compile time, and macros are closer to a dynamic language than to a typed language (in fact, macros are interpreted) Anyway, I'm just throwing more ideas to the table :-) |
@RX14 is far more flexible to allow the attribute declaration (if any) to leave the semantic aside. There are many use cases and some might not be expressed as "if I apply this attribute I need to do this to the target" so you might not know what to do. For example: you might need to access the class attributes when applying something based on ivar attributes. |
@asterite I agree to be as lightweight as possible. Before jumping in a c#-ish style I tried to imagine what would be a ruby-like for this. I think that been able to discover possible attributes/options would open the possibility for better editor/ide support in the future. Again, we could have a I agree that is neither a struct nor a class and using them is a hack. Maybe Regarding type safety for properties, I would say that not typing the properties is fine. Everything will need to be interpreted in macro mode as you said. I did think of having required/allowed properties. I see them as if when you call a method without type annotations they have have required and allowed named arguments. So, I am not fully convinced of the |
@bcardiff Yes, I used the Regarding automatically doing stuff at compile-time if an attribute is present, maybe we can have another property of an annotation, and that is to trigger some macro call inside the type. So something like this: @[JSON::Serializable]
class Point
@[JSON::Attribute]
property x : Int32
@[JSON::Attribute]
property y : Int32
end I used @[Attribute(JSON::Serializable, finished: JSON.define_mapping)] Here Maybe an attribute that is applied to an instance variable can also have a class Point
@[JSON]
property x : Int32
@[JSON]
property y : Int32
end which is a bit more DRY: if we use a JSON attribute on an instance var, it's obvious that we want the type to be serializable, so we want to generate the necessary methods. |
What about something like:
|
I think that the syntax for custom attributes should be exactly the same as the current attribute syntax. Whether attributes are implemented in the compiler or using macros, they are simply metadata which add info to what they are annotating, which may cause a behavioural change. The implementation details (compiler or not) should not be obvious. I also prefer repeating annotations to having them seperated with commas. I think that having them on the same line means multiple (possibly unrelated) concerns per line. @[JSON(key: "foo")]
@[YAML(key: "bar")]
property foo : String vs @[JSON(key: "foo"), YAML(key: "bar")]
property foo : String I also think that attributes should be explicitly declared, with exactly the semantics and syntax for parameters in macro declarations (just what's in the brackets). That is, define the possible names, and their default values, but not their types. Splats are also obviously allowed, and adding a default value would make the parameter optional. Sometime like I strongly dislike @asterite's syntax where declaring an attribute was done inside the attribute syntax itself. ( I do think we should be able to define a macro which runs when an attribute is applied, because it removes the need to have both the annotation to describe the data, and to manually apply the macro to use the data. Whether this is done by naming the macro which should be run when the attribute is applied in the attribute definition, or by making the attribute definition a macro in itself, I don't mind. |
It strikes me as just imitative -- copying Go or Java/C# -- the approach seems quick and dirty and is clearly ugly. Embedding this information into classes leads to metadata bloat and/or an explosion of names (a subclass for each possible serialization). My gut feeling is that an improved macro system is where an elegant solution lies, but I am not sure. Ideally there would be a clean SOC between Crystal classes and the transformation code that converts serializations to/from them. |
I solve this by magic finished macro, and shards: https://github.com/kostya/auto_constructor, https://github.com/kostya/auto_msgpack, https://github.com/kostya/auto_json require "auto_json"
require "auto_msgpack"
struct Person
include AutoJson
include AutoMsgpack
field :name, String
field :age, Int32
field :email, String?, json_key: "mail"
field :balance, Float64, default: 0.0, json: false
field :data, String?, msgpack: false
end
person = Person.new(name: "Vasya", age: 20, balance: 10.0, email: "bla@ru")
p person # => Person(@age=20, @balance=10.0, @data=nil, @email="bla@ru", @name="Vasya")
json = person.to_json
puts json # => {"name":"Vasya","age":20,"mail":"bla@ru"}
person2 = Person.from_json(json)
p person2 # => Person(@age=20, @balance=0.0, @data=nil, @email="bla@ru", @name="Vasya")
msgpack = person2.to_msgpack
puts msgpack # => Bytes[132, 164, 110, ...]
person3 = Person.from_msgpack(msgpack)
p person3 # Person(@age=20, @balance=0.0, @data=nil, @email="bla@ru", @name="Vasya") |
I've tried to do something similar to @kostya, but my problem was the class User
macro finished
puts "1, 2, 3"
end
end
sleep(5)
# Won't print "1, 2, 3" until *after* the sleep is done. Is there a way to "force" the finished hooks to run?"
# When it's a server the "finished" macro *never* gets called |
@paulcsmith the You can see that finished macro works with the following example: class Foo
macro finished
BAR = 10
end
end
p typeof(Foo::BAR) # => Int32
p Foo::BAR # => 10 If you replace the class Foo
macro finished
puts "1, 2, 3"
end
end
puts "main"
Fiber.yield
|
@luislavena Ah ok I'll try this out. Thanks for the explanation and example code! |
Hi all, hope i can jump in. I've been programing c# for over 8 years, eventually moved away from Ruby. I'd love it if it is possible to have typed attributes exactly like c#. And if possible also drop the [] it looks like weird clutter to me, totally unruby, more like machines then for humans to be honest.
Or + ? Hmm not sure about this one.
I'm writing a graphql crystal and annotations/attributes would really be handy, but please make them for humans <3. (even if it means making language changes) |
What about |
I love this proposal! This is one of my favorite parts about Go’s marshaling/unmarshaling. |
Why not associate custom attributes with macros? @[Attribute(name: "TestAttr")]
macro test(node)
# do something with node...
node
end
@[TestAttr]
def abc
end
# test gets called with this Def node, and its result is pasted in place of it |
@zatherz this can already by done with macros: macro test(node)
# do something with node...
node
end
test def abc
end
# test gets called with this Def node, and its result is pasted in place of it https://carc.in/#/r/2i3u |
yes, you missed the attribute syntax @[Attribute(name: "TestAttr")]
macro test(node, test)
# do something with node...
node
end
@[TestAttr(test: 1)] # node would always be passed through the `node` argument?
def abc
end
# test gets called with this Def node and 1, and its result is pasted in place of it |
Can be done like this: test test: 1, def abc
end But that's something entirely different anyway than what this is about. I don't think it would make sense to implement arbitrary annotations as macros. |
Bump. |
right now there is one type of meta information exists, have variable default value or not, in case: class A
@a : Int32 = 1
end can i access to such information from macro (have variable default value or not), to construct macro like mappings and others? or how hard to add such info to |
So, I've been thinking a bit more about this, and here's a proposal. Declaring an attributeYou do that with an module JSON
attribute Field
end
end Right now the atrribute definition will be empty, in the future maybe we can specify there to what it applies (types, ivars, etc.), maybe what keys it can hold, etc. One can document an attribute as usual, and it will appear in the docs. We can even go one step further and make the current attributes defined like that, but be "primitives" that affect compilation (like they currently do). The difference is that they will show up in the docs. (in that way, I'd also like to make Applying attributesYou do it like you do it now: class Foo
include JSON::Serializable
@[JSON::Field(key: "ex")]
property x : Int32
end Note that Except that right now Querying attributesYou use the With that, we could implement class Foo
@[JSON::Field(ignore: true)]
@x : Int32
end I think it's better to choose attributes to drop in serialization, as usually we'd like to serialize all attributes. It also makes it easier to serialize to JSON, YAML and other formats: you don't need to specify that attribute again and again for all the fields and the different formats (you do have to do that if the Advantages over the current
For now you could attach attributes to types and instance vars. Maybe in the future we could also attach them to methods. Serialization is just one example for attributes. I'm pretty sure people will come up with great examples using them. Thoughts? |
I don't think it makes sense to have an attribute definition without at least containing the possible arguments to the attribute. Having an Apart from that, looks fine to me! |
how to acces to attributes from different modules? class Foo
@[JSON::Field(key: "y")]
@[YAML::Field(key: "z")]
@x : Int32
end here double |
@kostya for example: {% for ivar in @type.instance_vars %}
{% attr = ivar.attribute(JSON::Field) %}
{% key = attr ? attr[:key] : ivar.name %}
{% end %} |
Closed by #6063 |
While trying to think how to have
JSON.mapping
work with class inheritance, being able to add new properties to the mapping of a subclass, I concluded that the easiest way to implement this would be to attach metadata to instance variables. That way one would need to traverse these instance variables to generate the mapping. This will work through deep hierarchies, even with included modules and so on. This is also similar to how most other languages do JSON mapping, like Java, C# and Go. We actually discussed this in the past with @waj but thought that we would need to define these attributes somewhere, like in Java and C#... but maybe it can be done without all of that complexity (more similar to Go).So I thought of something like this:
So a
Meta
attribute has a namespace (a symbol) and optional data.To traverse the instance vars one can do:
Maybe we can even provide a method to only get instance vars with a present attribute:
What's missing in the above snippet is the code that defines
new(pull : JSON::PullParser)
andto_json(io)
. Maybe one can include aJSON::Mapping
module or similar:Now, the topmost snippet doesn't define getters and setters, one has to do that manually:
However, since
property
expands to an instance variable declaration, the Meta attribute would apply to the macro expansion, so to the instance variable, and we can do:Maybe writing all that
@[Meta(...)]
is a bit tedious, so we can have aJSON.attribute
macro that would expand to@[Meta(:json)]
(this is just a convenience syntax):We could still keep
JSON.mapping
, which will expand to a series of properties with the@[Meta(:json)]
attribute, and also include theJSON::Mapping
module (I think the currentJSON.mapping
syntax is very short and convenient when mapping to an existing API you want to consume)Now a subclass of Point needs only to add more properties and map them:
Because the
new(pull : JSON::PullParser)
method that was generated uses@type
inside it, it's a macro method, so this method will be different for Point3 than from Point, taking into account this new instance variable.Maybe in the future we'd like to apply
@[Meta]
tags to methods and types, so we should probably have an extra parameter that specifies the valid targets. For example:Basically,
@[Meta(key, optional_data, optional_target)]
.And of course one could query this metadata from types and methods as well. One valid examples of method metadata is a test framework, where you would annotate methods with
@[Meta(:test)]
to later know which methods to run.Another good example of instance var metadata is an ORM. Right now one has to keep that metadata separated from the instance vars, but keeping it in the instance vars is probably the best/easiest way to do it.
Also, this reduces duplication when defining both JSON and YAML mapping:
The second form duplicates the types, and the macro also generates properties for each of those variables, so a lot of duplicated code under the scene (it's not a big deal, but the first form is more DRY)
I believe this approach will reduce code complexity a lot. In fact, with this we can implement extensible JSON mapping without using the newly
finished
macro hook, but that doesn't meanfinished
is useless, it can have other uses (like, when you need to store stuff that isn't necessary attached to instance variables, methods or types, and process that at the end).This is just a preliminary design, I haven't discussed this with anyone else, so comments are welcome!
/cc @bcardiff @waj
The text was updated successfully, but these errors were encountered: