String to Symbol

Hi!

I’m trying to generate symbol from string, but I don’t know what I’m doing wrong.
Consider the code below:

macro to_sym(sym)
  :{{sym.id}}
end

my_string = "abc"

pp! to_sym(my_string) # to_sym(my_string) # => :my_string

I expected the result of the code above to be :abc.

This code is available on the playground: Carcin

The reason is you can’t use non macro variables as an argument to a macro. I.e. when the macro expands it’s working based on the name of variable, not its value because the value of the var is not available at compile time. It would work the way you want if you did to_sym("abc"). Also could do sym.symbolize instead of :{{sym.id}}.

However, what’s the reasoning for wanting to do this? Why not just use a String? It’s usually recommended to avoid symbols as they have essentially no benefit over a String or Enum. See More on Symbols.

Yeah, macros and symbols are a bit confusing if you’re coming to Crystal from a language like Ruby or JS. The main thing to remember about macros is that they work on expressions, not values, so you weren’t passing the value "abc" to the macro, you were passing the expression my_string. You can see the difference if you pass a less simple expression:

macro foo(expr)
  {% pp expr %}
end

my_string = "abc"

foo(my_string * 2)

At compile time (not run time!), this will output my_string * 2, not "abcabc".

Symbols are another source of confusion if you’re used to Ruby because Ruby lets you generate them at runtime and they can be garbage-collected. The main difference between symbols and strings in Ruby are mainly developer ergonomics — ironically, though, they cause plenty of other problems.

Crystal symbols, on the other hand, must be known at compile time and are nothing like strings. The only times I’ve used them in the past 4 years or so were:

  • iterating over NamedTuple instances, which were almost always the result of **kwargs, which you should also use sparingly, but even then I’m not writing symbol literals
  • as a shorthand for enum values (for example, response.status = :not_found as shorthand for response.status = HTTP::Status::NOT_FOUND)

Basically, symbols require low-cardinality and must be known at compile time, and the only benefit over a string is constant-time equality checks (basically, the microest of micro-optimizations) in very specific circumstances.

1 Like

Yes, and for typical use cases where that matters, there’s enum types in Crystal which are a type-safe alternative to symbols. Only downside is that the values need to be predetermined and can’t be added on the go. That’s rarely relevant, though.

3 Likes

I changed my mind and now I’m using Strings