Crystal equivalent to c# nameof expression?

C# has a handy nameof expression which allows you to get the string representation of a variable, type or member so you don’t need to use string literals to refer to them. Like this:

// This is C#, not Crystal
readonly record struct Hiker(string Firstname, string Lastname, int Age);

var hiker = new Hiker("Arthur", "Dent", 42);

var x = nameof(hiker); // => "hiker"
var y = nameof(Hiker); // => "Hiker"
var z = nameof(Hiker.Age); // => "Age"

Reference: The nameof expression - evaluate the text name of a symbol - C# reference | Microsoft Learn

Does Crystal have something similar to this? Is it doable by using a macro?

1 Like

I haven’t tried this before, but I think so.

require "json"

macro nameof(name)
  {{name.stringify}}
end

record AStruct, something : Array(String) do
  include JSON::Serializable
end

def a_method
  "hello"
end

hello = a_method

p nameof(a_method) # => "a_method"
p nameof(hello)    # => "hello"
p nameof(AStruct)  # => "AStruct"

The only problem with your macro is that properties off of Objects would return full path (e.g. nameof(hiker.Age) => “hiker.Age” in Crystal). I dont think its possible without doing some regex to remove everything before the last period (so you could handle something like nameof(hiker.family.father.age)).

Small change to @Sunrise’s:

require "json"

macro nameof(name)
  {% if name.is_a?(Var) || name.is_a?(Path) %}
    {{ name.stringify }}
  {% else %}
    {{ name.name.stringify }}
  {% end %}
end

record AStruct, something : Array(String) do
  include JSON::Serializable
end

def a_method
  "hello"
end

hello = a_method

p nameof(a_method) # => "a_method"
p nameof(hello)    # => "hello"
p nameof(AStruct)  # => "AStruct"
p nameof(a_method.to_s) # => "to_s"

You can prefer the nodes name over stringifying the whole thing, which should handle the case you mentioned @mjblack

That works but I was going for a more simplified approach since it looks like C# doesnt care if the name is a valid variable or not. Please dont judge me harshly for my simplified version. :laughing:

macro nameof(name)
{{name.id.stringify.split(".").last}}
end

Thank you, seems to work very well! The only change I needed to do was to split the string and get the last element (when there are multiple “levels” of references):

macro nameof(name)
  {{name.stringify.split('.').last}}
end

record Backpack,
    contents : String
record Hiker,
    firstname : String,
    lastname : String,
    age : Int32,
    backpack : Backpack

hiker = Hiker.new("Arthur", "Dent", 42, Backpack.new("towel"))

pp nameof(hiker) # => "hiker"
pp nameof(Hiker) # => "Hiker"
pp nameof(Hiker.age) # => "age"
pp nameof(Hiker.backpack.contents) # => "contents"