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

Allow multiple annotations in the same annotation block #11483

Open
beta-ziliani opened this issue Nov 22, 2021 · 34 comments
Open

Allow multiple annotations in the same annotation block #11483

beta-ziliani opened this issue Nov 22, 2021 · 34 comments

Comments

@beta-ziliani
Copy link
Member

Currently, each annotation takes its own line, leading to code that is not as comfortable to read. Made up example:

@[Nullable]
@[Unique]
@[Default(0)]
@[Type(smallint)]
property MyKey

@[NonNullable]
@[Default("")]
@[Type(string)]
property Data

Consider instead the following version:

@[Nullable, Unique, Default(0), Type(smallint)]
property MyKey

@[NonNullable, Default(""), Type(string)]
property Data

To me, I can easily read that there are two properties, and I can check the details just if I need to. In the first version, I have to go through more lines to get the most important information.

@naqvis
Copy link
Contributor

naqvis commented Nov 23, 2021

I would say this would cause confusion as current way of annotating is how other languages are following the practice. Also this approach will cause clutter when combining multiple annotations which comes with elements.

instead it would be much cleaner if one can annotate an annotation and then use that annotation instead. Something like (made up example)

@[Nullable]
@[Unique]
annotation MultiAnnotation
end

@[MultiAnnotation]
property mykey

@straight-shoota
Copy link
Member

straight-shoota commented Nov 23, 2021

This syntax should be doable, but I'm not convinced it's a good thing. I would strictly prefer each annotation on a single line. That makes more sense to me to clearly tell them apart. It's easier to miss an annotation if it's hidden behind others.

Maybe this can be combined to writing annotations in a list, but formatted with line breaks?

@[Nullable,
  Unique,
  Default(0)
  Type(smallint)]
property MyKey

@[
  NonNullable,
  Default(""),
  Type(string)
]
property Data

Not sure if that has a reasonable benefit over the original multi-line notation.

@HertzDevil
Copy link
Contributor

We should allow multiple annotations on the same line:

@[Nullable] @[Unique] @[Default(0)] @[Type(smallint)]
property my_key

There is no need to expand the syntax of a single annotation node to allow this.

@Sija
Copy link
Contributor

Sija commented Nov 23, 2021

We should allow multiple annotations on the same line:

@[Nullable] @[Unique] @[Default(0)] @[Type(smallint)]
property my_key

That's IMO the least readable version of all proposed.

@beta-ziliani
Copy link
Member Author

I don't know if it's the least readable, but I certainly believe the original proposal reads much better

@vlazar
Copy link
Contributor

vlazar commented Nov 23, 2021

Putting aside the typical semantic of | and considering only visual representation I like how this looks in terms of readability:

@[Nullable | Unique | Default(0) | Type(smallint)]
property MyKey

@[NonNullable | Default("") | Type(string)]
property Data

Compared to

@[Nullable, Unique, Default(0), Type(smallint)]
property MyKey

@[NonNullable, Default(""), Type(string)]
property Data

Sure, you can write

@[Nullable , Unique , Default(0) , Type(smallint)]
property MyKey

@[NonNullable , Default("") , Type(string)]
property Data

But it less readable and different style with commas.

@n-pn
Copy link

n-pn commented Nov 24, 2021

how about using semi-colons as separator? Some people like me has been grouping related Enum options into a single line like this already, so our eyes are already trained for this..

@[Nullable; Unique; Default(0); Type(smallint)]

also, nothing can stop us from using multiple annotations when it's more appropriate, like this:

@[NonNullable; Default(0); Type(int)]
@[Unique; UniqueConstraintName("unique_index")]
property name

@spuun
Copy link

spuun commented Nov 24, 2021

To me the brackets make it look like i could put more stuff in there, like in c# and PHP.
If it was only @MyAnnotation (like in java?) it wouldn't make me think that I could put multiple annotations in one block (because there's nothing that can group the annotations).

@beta-ziliani
Copy link
Member Author

I'm failing to see the issue with the comma-separated list from these examples. I do agree it might be an issue with multiple arguments, but those aren't so common. And the notation [a, b, c] is consistent with a list.

@straight-shoota
Copy link
Member

I'd like to learn a bit more about real use cases. The examples are just made up.

In my projects folder, I found only a hand full of more than two consecutive annotations. They are from https://github.com/ujjwalguptaofficial/shivneri and https://github.com/athena-framework/athena, both frameworks which heavily use annotations.

I'm showing one example of each. They have the max number of consecutive annotations, but the complexity and length of each single annotation is representative.

https://github.com/ujjwalguptaofficial/shivneri/blob/bafc1b03ca55b1acaaff97aa7e40d816c62a60b0/tests/general/src/controllers/user_controller.cr#L39-L44

@[Worker("POST")]
@[Route("/")]
@[Guards(UserValidator)]
@[Inject("as_body")]
@[ExpectBody(User)]

https://github.com/athena-framework/athena/blob/bfba452dd40bfd515c9358723fd7e8a64bbf7f8f/spec/controllers/routing_controller.cr#L88-L90

@[ATHA::Get("events")]
@[ATHA::ParamConverter("since", converter: ATH::TimeConverter)]
@[ATHA::QueryParam("since")]

These real annotations are noticeably longer than those in the made up examples and occupy a higher fraction of the line. Which reduces the space advantage and would affect the readability of collapsed notation in a single line.

In my opinion, none of these examples would improve by collapsing the annotations, regardless of which syntax we use.

@oprypin
Copy link
Member

oprypin commented Nov 24, 2021

My main input here is that in the past years I've seen auto-formatters of all sorts usually converge on a "single thing per line" syntax.

In an alternate universe where Crystal already had both syntaxes, I'd expect some amount of complaints coming in from teams being unable to conclude on a consistent way to write these.

Even from the main post of this issue, the first example is actually the way that I'd prefer things converged on. And it has the huge advantage of being the only way to write it.

And this isn't even fixable by Crystal's formatter, the point of which is to have one undisputed way to write things.

@spuun
Copy link

spuun commented Nov 24, 2021

This is an example were I wanted to put multiple annotations in one block.

I made a config class were i wanted to annotate some properties if they could be changed on reload, should be included in the diff between current and new config, and if it can be set via command line argument. I can also imagine that I'd like to add some validation related annotations at some point.

It looks something like this (but a lot more options):

class Config
   @[NoReload]
   @[NoDiff]
   @[CliOpt("-d dir", "--data-dir=dir", "Data directory")]
   property data_dir = ""

   @[NoDiff]
   property users = Hash(String, String) .new

   property log_level = Log::Severity::Info
end

IMO it would have been more readable if i could put the simple annotations in on block. Now it almost feels like the properties disappears because of the annotations.

@beta-ziliani
Copy link
Member Author

And this isn't even fixable by Crystal's formatter, the point of which is to have one undisputed way to write things.

There is no such thing in programming :-) For instance, there are disputes already towards the use of unless and if at the end of the line:

if condition
  something
end

vs

something if condition

To me, shortening the number of lines (without breaking readability) is an important goal. When compared to Java, that was one of the main advantages of Ruby: no visual noise. And like @spuun mentions, annotations stacking up does introduce some noise to the general structure of the program.

@beta-ziliani
Copy link
Member Author

Regarding the examples shown by @straight-shoota , to me the first example benefits clearly from this:
https://gist.github.com/beta-ziliani/f1f73f14f19763ad02925e12ad397696

But again, it might be my biased love to reducing the number of LOCs.

@vlazar
Copy link
Contributor

vlazar commented Nov 25, 2021

We should allow multiple annotations on the same line:

@[Nullable] @[Unique] @[Default(0)] @[Type(smallint)]
property my_key

There is no need to expand the syntax of a single annotation node to allow this.

The fact that annotation in Crystal requires extra [] makes me a little bit sad. Otherwise combining annotations could be much cleaner:

@Nullable @Unique @Default(0) @Type(smallint)
property my_key

Or with commas

@Nullable, @Unique, @Default(0), @Type(smallint)
property my_key

@vlazar
Copy link
Contributor

vlazar commented Nov 25, 2021

I've found the ideal solution:

🔮Nullable 🔮Unique 🔮Default(0) 🔮Type(smallint)
property my_key

@asterite
Copy link
Member

I don't think we should design the language around reducing LOC

@vlazar
Copy link
Contributor

vlazar commented Nov 25, 2021

I don't think we should design the language around reducing LOC

Designing for that? Absolutely no! But there are multiline/single line options already to chose from. For example this looks very clean and natural to me in both forms:

# multiline
getter a
getter b
getter c

# single line
getter a, b, c

Annotation syntax is less clean as it is and with multiple annotations allowed on the same line it looks even less clean to me (relative to other Crystal code).

@beta-ziliani
Copy link
Member Author

beta-ziliani commented Nov 25, 2021

I don't think we should design the language around reducing LOC

No, but Crystal is designed to be clean, and this is what's this is about (not saying everyone should agree on what clear is in this particular topic). To me, shortening the lines it takes for annotation gives more space to understand the structure of the class, and that leads to cleaner code. But it should not be confused with the opposite "cleaner code -> fewer LOCs"; that's not what I mean.

@rymiel
Copy link
Contributor

rymiel commented Nov 25, 2021

I don't think we should design the language around reducing LOC

If that was the case, #9080 would have been accepted by now

@straight-shoota
Copy link
Member

The fact that annotation in Crystal requires extra [] makes me a little bit sad.

Yeah, I don't think they're technically necessary. Annotations are unambiguously determinated by parentheses (or whitespace if there are no arguments). Maybe we can remove them or make them optional? 🤔

Then instead of @[Nullable]; @[Unique]; @[Default(0)]; @[Type(smallint)] you could write @Nullable; @Unique; @Default(0); @Type(smallint) which is probably a bit cleaner. 🤷

@rymiel
Copy link
Contributor

rymiel commented Nov 25, 2021

I don't think they're technically necessary

Well, what was the reasoning behind this change from 7 years ago? #195

@beta-ziliani
Copy link
Member Author

BTW, let me add that annotations are akin to C#'s attributes, and in C# you can have multiple of them.

@asterite
Copy link
Member

Right! I think our annotations were inspires by those or Java's.

To be honest, I thought it was already possible to specify multiple annotations like that.

@vlazar
Copy link
Contributor

vlazar commented Nov 25, 2021

Right! I think our annotations were inspires by those or Java's.

@asterite Can you shed some light why they were using closer to Java syntax from the beginning (@:Name) but then changed to @[Name] in #195?
Now they look exactly like mix of Java + C# :)

@asterite
Copy link
Member

I can barely remember what I did last week, I can't remember why we changed the syntax 7 years ago. I think the old syntax looks very weird.

@vlazar
Copy link
Contributor

vlazar commented Nov 25, 2021

Reading more C# attributes examples I like it's syntax where you can combine multiple attributes in different ways like:

  • [In][Out]
  • [In, Out]

I also like that without @ both of these ways look cleaner, especially the first one (or multiline). Also because @ is already used for variables in Crystal.

@[Nullable] @[Unique] @[Default(0)] @[Type(smallint)]
property my_key

@[Nullable]
@[Unique]
@[Default(0)]
@[Type(smallint)]
property my_key

vs

[Nullable] [Unique] [Default(0)] [Type(smallint)]
property my_key

[Nullable]
[Unique]
[Default(0)]
[Type(smallint)]
property my_key

@Blacksmoke16
Copy link
Member

My 2 cents, I think it would be worth doing. It's worth pointing out that this new syntax isn't going to replace the old syntax. I.e. I don't think anyone is saying that once/if this feature is added that you must use it. Although having the ability to use it would be helpful.

It would allow you to group related annotations by line while still being able to use multiple lines for distinct groups. For example If you had an ORM you could imagine something like:

@[Column]
@[Id]
@[GeneratedValue("AUTO")]
getter! id : Int64

with this feature you could rewrite it to:

@[Column]
@[Id, GeneratedValue("AUTO")]
getter! id : Int64

So you end up saving a line and also make things more readable by allowing multiple related annotations on each line when it makes sense.

@beta-ziliani
Copy link
Member Author

@vlazar note that [Deprecated] is a list with the attribute Deprecated, so the C# syntax couldn't fly here.

@HertzDevil
Copy link
Contributor

HertzDevil commented Nov 25, 2021

One thing not mentioned is the ability to ignore the newline between the annotation and the annotated node. This makes some things possible:

def fact(x)
  if @[Likely] x > 1
    x * fact(x - 1)
  else
    1
  end
end

Contrast with C++:

int fact(int x) {
  if (x > 1) [[likely]] {
    return x * fact(x - 1);
  else {
    return 1;
  }
}

Crystal's formatter, the point of which is to have one undisputed way to write things

If we do allow commas within @[], it is very likely those nodes would not be parsed as multiple Annotation nodes but as some other new composite AST node; the "undisputed" way to write things would continue to preserve both Annotations and those composites, since they are different nodes. This on the other hand makes some kinds of macros more complicated (e.g. when introspection on ClassDefs is eventually supported), in the same way macros for regular code need to account for Expressions's. That is another reason I believe we should not change the abstract syntax of annotations when a slight relaxation to allow spaces in addition to newlines between adjacent annotations will do the job.

@oprypin
Copy link
Member

oprypin commented Nov 25, 2021

@HertzDevil I think your concern is very easy to resolve. Just have the compiler convert comma-separated annotations into multiple annotation nodes, as syntax sugar at very early stages of compilation.

@qualterz
Copy link

qualterz commented Jul 6, 2022

crystal-annotations-meme

@asterite
Copy link
Member

asterite commented Jul 6, 2022

@qualterz great observation! That's exactly why we chose that syntax. It would be, at least partially 😉, familiar to Java and C# programmers

@HertzDevil
Copy link
Contributor

Fun fact: Ruby RBS chose %a{...}.

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

No branches or pull requests