crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.49k stars 1.62k forks source link

`HashLiteral` and `NamedTupleLiteral` QoL #14083

Open Blacksmoke16 opened 11 months ago

Blacksmoke16 commented 11 months ago

Hash and NamedTuple literals in macro land are a bit unique in that they are both mutable data structures, but with a few key differences:

  1. NamedTupleLiterals allow accessing the value of any key no matter its type, while HashLiterals require the key to be of the same type it was defined with:
{%
  hash = {"foo" => 10}
  pp hash["foo"]    # => 10
  pp hash[:foo]     # => nil
  pp hash["foo".id] # => nil

  nt = {foo: 10}
  pp nt["foo"]    # => 10
  pp nt[:foo]     # => 10
  pp nt["foo".id] # => 10
%}
  1. There is no way to create an empty NamedTupleLiteral without using a literal, which is useful when you want to support the liberal key access they offer.
{%
  a = {} of Nil => Nil               # => HashLiteral
  b = Crystal::NamedTupleLiteral.new # => undefined macro method 'TypeNode#new
  c = {foo: nil}                     # => NamedTupleLitearl
%}
  1. Because of the previous issue, it's not really possibly to create them dynamically, e.g. keyed by some value retrieved from an annotation or something without initilizing it to something like c = {__nil: nil}, but because https://github.com/crystal-lang/crystal/issues/8849 is still TBD you're kinda stuck with dealing with that key/value.

Not really sure what I'm proposing, but at least wanted to start a discussion on possible ways to make these types easier to work with. Whether that be normalizing the key access between the types, or allow creating empty namedtuple literals, or something else.

HertzDevil commented 2 months ago

Since Annotation#named_args always returns a fresh NamedTupleLiteral even in 1.0.0, you could do this:

macro foo(__named_tuple = @[UNUSED])
  {{ x = __named_tuple.named_args }} # => {}
  {{ x.class_name }}                 # => "NamedTupleLiteral"
  {% x[:d] = 1 %}
  {{ x }}                        # => {d: 1}
  {{ __named_tuple.named_args }} # => {}
end

foo
Blacksmoke16 commented 2 months ago

Oo that's a good find! For my use case tho, I don't have access to a macro parameter like that. But I did test it and:

annotation UNUSED; end

@[UNUSED]
module Test; end

...

Test.annotation(UNUSED).named_args

Seems to work the same so you could get a new one anywhere.

Tho in one place outside of macro code I still need to do:

CONFIG = {parameters: {__nil: nil}} # Ensure this type is a NamedTupleLiteral

But this is a slightly diff use case than this issue is for at least.