Equivalent of Ruby's `self.class::Subclass`

I would like to convert this Ruby code to Crystal:

# Ruby code

class P
  class Sub
    def m
      "from P::Sub"
    end
  end
  
  def sub_m
    self.class::Sub.new.m
  end
end


class C < P
  class Sub
    def m
      "from C::Sub"
    end
  end
end


puts C.new.sub_m # => "from C::Sub"

By using self.class::, C.new.sub_mreturns “from C::Sub” instead of “from P::Sub”, but this syntax is not valid in Crystal: Error: unexpected token: "::"

https://play.crystal-lang.org/#/r/icna

Can you share a bit more about your use case? There may be a more Crystal way of doing what you want to do without needing this Ruby feature.

EDIT: I just realized this is basically Crystal macros can be used for:

def sub_m
  {% begin %}
    {{@type}}::Sub.new.m
  {% end %}
end

This’ll make the compiler define sub_m on each child of P with the subclass’ name replacing {{@type}}.

See Macro methods - Crystal.

Thank you,

Are {% begin %} and {% end %} mandatory? It’s a heavy syntax.

My use case: I need to download very similar files from different sources, most of the sources respect the naming conventions but some don’t, so I just overwrite the method in the subclass that deals with the naming. Some pseudo code:

class Downloader
  class OutputFile
    def new_from_url(url : URI)
      # extract some variables from the url
    end
    
    def local_path : Path
      # construct the local path with the url variables.
    end
  end
  
  def download(url : URI)
    output_file_path = OutputFile.new_from_url(url).local_path
    # download and save as output_file_path
  end
end

# Source A respects the naming convention, nothing to modify
class SourceA < Downloader
end

# Source B uses a different naming, I only modify `OutputFile#new_from_url`
class SourceB < Downloader
  class OutputFile < Downloader::OutputFile
    def new_from_url(url : URI)
      # custom code to extract the variables from the url
    end
  end
end


Yes, because of Macros - Crystal. But you only need to do that once so it’s not too bad.

Is probably another way to go about this tho if it’s a deal breaker for you tho.

It wasn’t clear what you were pointing to here because that link talks about constructs like case statements. It surprised me, too, that you need the explicit delimiters because {{@type}} is perfectly legal without them in a lot of other expressions, like {{@type}}.foo. It’s specifically {{@type}}::SomeConstant that breaks. The macro code (namely, {{@type}}) is legal without the delimiters, unlike the macro code in the case statement example.

After experimenting with a bit, it looks like the Crystal parser doesn’t know how to parse {{@type}}::Foo, and when it sees {% begin %} and {% end %} it defers parsing everything in between until after a macro pass, after which I imagine the {% begin %} and {% end %} would be consumed and the {{@type}} interpolation would have been processed into each individual type that called that method. Is that accurate?

Yea, meant this part:

When writing macros (especially outside of a macro definition) it is important to remember that the generated code from the macro must be valid Crystal code by itself even before it is merged into the main program’s code

So because {{@type}}::Foo isn’t valid, you need the {% being %}/{% end %}. The compiler treats it the same as {% if true %} and evaluates it as one “group” so the output is the valid even if the macro expression itself isn’t.

{{@type}}.foo works I’d assume because from the parser side of things it’s nothing more than calling an instance method on a Tuple of a tuple of some ivar which in totally valid unlike {{@type}}::Foo

This is at least the mental model of how I think it all works under the hood.

In {{@type}}.foo the macro expansion parses as a distinct node which composes the receiver of a call. This node can be expanded independently.
{{@type}}::Foo however would be parsed as a single path node. But that path is not valid.

It might be possible to add parser support for this specific use case, though. Could be worth exploring.

1 Like

Why is self.class::Foo invalid? self.class.foo works fine.

Basically for the same reason as before. self.class::Foo isn’t a valid Path, while self.class.foo is a valid Call.

self.class is a runtime expression. It depends on the value of self in the current scope (in the OP example, it could be C or P)
That means we can only evaluate it at runtime. Thus it cannot be part of a path, which is a compile time value.

This is one of the cases where it’s impossible to represent Ruby’s very dynamic and flexible runtime exactly in a compiled language.

But Crystal offers macros, and specifically {{ @type }}, to achieve a very similar effect, entirely at compile time.

Thank you for the explanation.

I played around with the macros and found two alternatives without the {% begin %} {% end %} block.
{{ @type.name + "::Sub" }}
{{ "#{@type}::Sub".id }}

1 Like

{{ @type.constant("Sub").name }} should work as well.

2 Likes

This is a really good explanation. Unfortunately, that parser behavior is unintuitive to someone who doesn’t know the parser internals. To my eyes, the macro is only {{@type}} and it seems like it should interpolate that correctly.

I agree. I guess it’s a limitation of the parser and ast to accept macros inside type paths, and semantic to be capable to expand the macro when resolving the path, which might be hard.

I suppose we could add parser support by having the parser convert a path including a macro expression into an equivalent macro expression.
So {{@type}}::Foo would parse as something similar to {{ "#{@type}::Sub".id }}.
That would work within a very limited scope.

1 Like

The :: is syntactically closer to an operator than part of a path in Ruby, so it is reasonable for Rubyists to expect {{ ... }}::Foo to be valid Crystal syntax.

3 Likes

IMO, the Language Guide should have an explicit section about these “paths” and the :: syntax.