-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow user-defined macro methods #9091
Allow user-defined macro methods #9091
Conversation
As much as I dig the feature, the idea of merging it without the support from its author seems off. It's not only bells and whistles but also, fixing potential bugs, uncovered edge-cases and future maintenance - all of which require will and understanding the code. If @asterite doesn't feel confident about it, then who will? |
Oh, I feel confident about the code. I just think we need to think this a bit more. Reusing the |
@asterite By it I meant the whole feature (incl. the design), not just the implementation. |
It depends on how we want to view this feature. On one hand it can be seen as an MVP that is a pretty small diff, backwards compatible, and useable. On the other it could be viewed as a proof of concept of something that could be refined/improved upon more. IMO both of these are true however, due to the reason mentioned earlier, it would be more helpful to release this MVP, have people try it out, and let that inform future possible iterations. Otherwise I fear that it'll be stuck in the perpetual design phase. Given I don't see this being super commonly used and breaking changes are likely to be small; it's probably fine to gather feedback and address any issues/refactors at a later point. Either way, it's a bit out of my control so we'll just see what the other core devs opinions are. |
I see this more like a PoC for now. That means it should either be merged after 1.0. Or behind a feature flag. But waiting for 1.0 is certainly the easier option. |
I would be in favor of a feature guard to at least allow some of my work to move forward. However, requiring all users to build their application with that flag is less than ideal. Also it would be even worse if the feature was later reverted. So I suppose the best course of action would be waiting until after This feature would be a game changer in what is possible via macros (for the better). I personally have a vested interest in the feature as it's blocking some refactors/features I wanted to implement within Athena. Because of this, I would be happy to sponsor this issue or something if that would help address some of the design concerns and/or help make it more of a priority/first class citizen. Either way I guess I'll just keep this open and see how things play out. |
@Blacksmoke16 Could you provide a list of things you could do with this that you can't do right now? |
@asterite Sure. I'll use https://github.com/athena-framework/dependency-injection as an example. The two big points are:
To be clear, based on #8835 I'm more interested in a way to work directly with the In the end because of these two points, you end up with code that is quite gross. With this PR to address those previous points, I'm now able to refactor things to be much cleaner. The logic is broken out into "methods" so it's more readable, and reusable; and quite minimal considering what it's doing. Another feature which this PR enables is the ability to alter the service container while it's being compiled, similar to Compiler Passes in Symfony. An example of how this could be used is like:
AND, since I have things broken out into "methods", users who implement these compiler passes are able to reuse the same logic that used in the core shard. All this macro code would make you think its doing some hacky stuff or something. However, in the end its just defining some getters/ivars within a type. The complexity comes from auto resolving dependencies of each service, such as services depending on other services etc. # Use compiler pass the change the name
# that the `Foo` service will use
module ChangeNamePass
include ADI::CompilerPass
macro post_process(service_hash, alias_hash)
service_hash["foo"][:arguments][1] = "bob"
end
end
# An interface that could have multiple implementations
module SomeInterface
end
@[ADI::Register]
class FakeService
include SomeInterface
end
# Allow aliasing a specific interface to a given service
@[ADI::Register(name: "custom_fake", alias: SomeInterface)]
class CustomFooFakeService
include SomeInterface
end
# Auto resolves based on an interface but supply explicit arg
@[ADI::Register(_name: "JIM")]
class Bar
def initialize(@asdf : SomeInterface, @name : String); end
end
# Auto resolves based on type
@[ADI::Register]
class FooBar
def initialize(@obj : Foo); end
end
# Scalar arguments
@[ADI::Register(1, "fred", false)]
class Foo
def initialize(@id : Int32, @name : String, @active : Bool); end
end
# Multiple services based on same class
@[ADI::Register("GOOGLE", "Google", name: "google", tags: ["feed_partner", "partner"])]
@[ADI::Register("FACEBOOK", "Facebook", name: "facebook", tags: ["partner"])]
struct FeedPartner
getter id : String
getter name : String
def initialize(@id : String, @name : String); end
end
# Resolve all services with the given tag
@[ADI::Register("!partner")]
class PartnerManager
getter partners
def initialize(@partners : Array(FeedPartner))
end
end This code would produce: struct ServiceContainer
# Define getters for each service, if the service is public, make the getter public and also define a type based getter
private getter fake_service : FakeService { FakeService.new }
private getter custom_fake : CustomFooFakeService { CustomFooFakeService.new }
private getter bar : Bar { Bar.new(custom_fake, "JIM") }
private getter foo_bar : FooBar { FooBar.new(foo) }
private getter foo : Foo { Foo.new(1, "bob", false) }
private getter partner_manager : PartnerManager { PartnerManager.new([google, facebook] of Union(FeedPartner)) }
private getter google : FeedPartner { FeedPartner.new("GOOGLE", "Google") }
private getter facebook : FeedPartner { FeedPartner.new("FACEBOOK", "Facebook") }
# Define getters for aliased service, if the alias is public, make the getter public and also define a type based getter
private def some_interface : CustomFooFakeService
custom_fake
end
# Initializes the container. Auto registering annotated services.
def initialize
# Work around for https://github.com/crystal-lang/crystal/issues/7975.
Athena::DependencyInjection::ServiceContainer
@fake_service = fake_service
@custom_fake = custom_fake
@bar = bar
@foo_bar = foo_bar
@foo = foo
@partner_manager = partner_manager
@google = google
@facebook = facebook
end
end |
Wait, how does
Sorry, that's not being used in the code... it's also a lot of code so it's not easy to follow for me.
If what this PR enables is not having to repeat code, BUT you can still do thing by repeating code, then I think we should take our time to design this properly. |
It's defined at the top. It just accepts some arguments, and returns a
Yea sorry. It's used towards the bottom. One of the last thing that happens before creating the getters. See the bottom of my reply for some example code that shows the input/output.
The compiler pass concept is impossible currently. I tried implementing the
Don't get me wrong. I'm not suggesting rushing to merge this if it's something that could be reverted in the future. But I'd also rather not let this go by the wayside and be forgotten about or take years... |
I guess I didn't know this was possible: class Foo
macro baz
1
end
macro bar
{{@type.baz}}
end
end
p Foo.bar My idea was that you could do
Maybe that's a good thing. I think I introduced the macro I just wouldn't like to introduce more and more features without proper planning. |
True. I think we have some options tho:
However, doesn't this PR also allow you to add methods to existing class Crystal::Macros::StringLiteral
macro upcase
"foo"
end
end
macro bar
{{ "x".upcase }}
end
# Oh, why isn't this "foo"
pp bar # => "X" If we want to be strict, maybe the best solution would be to do 3 and disallow defining methods within existing types, like
I'm totally fine with this. Where do we start? 🙂 |
Once 1.0 is released |
Removes CompilerPasses as they aren't possible by default Limit array nesting to two levels
63e37f0
to
54436c8
Compare
def visit(node : While) | ||
while true | ||
node.cond.accept self | ||
break if !@last.truthy? | ||
node.body.accept self | ||
end | ||
false | ||
end | ||
|
||
def visit(node : Until) | ||
while true | ||
node.cond.accept self | ||
break if @last.truthy? | ||
node.body.accept self | ||
end | ||
false | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should live in their own PR as they would benefit regular macros too, although clearly there is a long way from implementing the full semantics of normal while
loops.
Closing in favor of a future implementation resulting from #8835 (comment). |
Reopens #8836, fixes #8835.
As it turns out adding
next
and changing the syntax tomacro def
is not a trivial endeavor. Because of this, and since these changes are BC, I propose to just merge the current state of this to at least have something.This is based on the original PR by @asterite. I added an additional commit to add some specs for the new
.each
and.each_with_index
macro methods as well.We can always iterate on this to improve it in the future. Considering the breaking change when that happens would be simple to fix, and I doubt this'll be used as much as some stdlib features; it's probably fine to handle that at a later time.