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: "::"
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
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?
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.
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.
I played around with the macros and found two alternatives without the {% begin %} {% end %} block. {{ @type.name + "::Sub" }} {{ "#{@type}::Sub".id }}
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.
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.