[RFC] Let `include`, `extend` and `require` be macros

Some time ago I realized that we really don’t need include, extend and require be keywords. They can be macros defined at the top-level, marked with @[Primitive] and handled specially in the compiler.

The end result is not too much. But we get these things:

  • Parsing is slightly easier because we don’t need to consider these cases
  • They will appear in API docs and we can document them nicely, in addition to documenting them in the Reference guide
  • One could define an include or require method in a class and use that without self. and it will work. For example, a spec library like Spectator could define an include method so one can do expect(subject).to include(something) (right now that’s not possible)

I played a bit with the compiler and this seems to be really easy to implement.

Do you think we should do it?

This thought is slightly inspired by this blog post I wrote.

8 Likes

Sounds brilliant! :love_you_gesture:

However, we’d need to take care about calling these macros from locations where they’re currently invalid, such as {% require "foo" %}. I could see some use case from calling require from a macro context if you want to load the source of that file into a macro variable. So it would probably be fine if it worked. But then it creates problems with resolving multiple require calls to the same file. So we probably need to keep some restrictions in place to make sure code from a required file ends up in the program.

1 Like

Oh, I don’t think {% require "foo" %} will work.

The way it will work is like this:

  1. You write require "foo"
  2. The compiler finds a macro require(path) at the top-level and tries to expand it
  3. The compiler finds it: it’s a primitive! The primitive is handled like this: expand the macro to the Require AST node we currently have
  4. That Require node is processed as usual

So the implementation is just “magic” that defaults to the current implementation.

Does that make sense?

2 Likes

Quick question: Will changing include to macro going to change existing semantics? For example now if we include some Module Foo and when we invoke obj.is_a?(Foo) this returns true. Will this behavior remain same after the change?

module Foo
  def foo
    "foo"
  end
end

class Bar
  include Foo
end

b = Bar.new
pp b.is_a?(Foo) # => true

Yes! Everything will remain the same. The only difference is that include will be listed in the docs, and it will be parsed like a regular call.

2 Likes

For the record: Adding these features to the API docs would work even without making them primitive macros. We could do the same as with pseudo methods (see src/docs_pseudo_methods.cr).

So both changes are technically independent. But the core is a change in how we view them.

Insidiously brilliant in enticing Rubyist to investigate Crystal! :wink:
Heck, even made me want to learn Crystal. :smile:
Also mentioned in this weeks Ruby Weekly.

https://rubyweekly.com/issues/592

2 Likes