Proposal: Introduce rule-name-centric calling convention, and deprecate return-type-centric Get
#18905
Replies: 22 comments 50 replies
-
So just to be clear, we still aren't calling the rule. We're just shifting the convention and the magic to look closer to normal code? |
Beta Was this translation helpful? Give feedback.
-
Oh and how does this work with providing an arg that isn't the request type, but is convertible through the engine to the request type? |
Beta Was this translation helpful? Give feedback.
-
I like the syntax as it feels and looks more concise. The described examples are a bit vague to me still though. Summarizing my understanding of it yields: # Anything after ellipsis is equiv. with regular `Get`
await the_rule_to_call(..., *args)
await Get(*args)
# Anything before ellipsis will be complemented with in scope params as needed
await the_rule_to_call(*args, ...)
await Get(*args) # adds params from scope =>
Get(ReturnType, {*args, Param1: param1, Param2: param2})
# Not using ellipsis must provide all required params for the call as the params in scope will not be consulted; this would be the preferred recommended way to call rules in order to fully specify the requirements?
await the_rule_to_call(*args)
await Get(*args) # w/o added params from scope Was this somewhat accurate? IIUC this merely directs the rule graph building to cut down the number of alternative rules for a dependency key, reducing the exponential explosion of variants going into monomorphization, at the cost of not being able to swap out rules by replacing backends, leaving I'm in favour of adding support for this call rule-by-name convention, but I'm vary of deprecating the |
Beta Was this translation helpful? Give feedback.
-
I imagine the same thing would hold as today for this case, i.e. the engine would need to go out and find the chain of rules to use to get the required type from what you provided. (edit: this is now #18905 (reply in thread) ) |
Beta Was this translation helpful? Give feedback.
-
It is only in order to be able to deprecate call rule-by-return-type that an alternative for As there is no single rule to call by name for a @union
class Car:
pass
class Volvo:
pass
Union(Car, Volvo)
# Old style:
await Get(Car, Volvo(...))
# New style, using the union type as the rule to call:
await Car(Volvo(...)) With the added benefit that it will be very clear at the call site that this is no regular rule we're calling but a union type. This has some implementation implications, but I think they are manageable. |
Beta Was this translation helpful? Give feedback.
-
Also out of curiosity, would this open the door for not using type-based returns in the engine for solving rule graph queries? We could endow the engine's representation of the return type with the name of the rule for differentiation. E.g. if we know the name of the rule, the following definitions are unambiguous in which one to use:
The engine creates a distinct type for each using the rule name(space) as a delineator, and rule parsing knows which should be used because now we're using the rule name. |
Beta Was this translation helpful? Give feedback.
-
I've only seen the ellipses How does this interact with type-aware tooling? Will we have to add |
Beta Was this translation helpful? Give feedback.
-
This is great! Re "There is no hardship involved in calling functions by name" - I think that is overly bullish. It does require knowing those names, whereas previously you only had to know about types (which you still need to know about). So it makes the surface area of our API dramatically larger. Previously all you had to know was that Type A can be converted to Type B, now you have to go look up the name of the rule that does so. I think this is still worth it for the huge benefits, but let's not gloss over the cost. Also, it means we can no longer change names at will, although that rarely happened anyway so truly is no hardship. We do have a one-time opportunity to change rule names, so we should probably come up with a naming convention for them that makes them easier to find/grok? |
Beta Was this translation helpful? Give feedback.
-
For consistency, it would be good if
|
Beta Was this translation helpful? Give feedback.
-
Also, in this proposal are the pre-ellipsis args truly positional? Today in |
Beta Was this translation helpful? Give feedback.
-
I'll also point out this eliminates a class of dev mistakes where you copy/paste some code and forget to change the rule names |
Beta Was this translation helpful? Give feedback.
-
One other potential syntax to throw into the mix would be something like: await the_rule_to_call(**implicit()) ... where |
Beta Was this translation helpful? Give feedback.
-
I'm guessing this also doesn't jive well with all the "inner rule generation" we do for |
Beta Was this translation helpful? Give feedback.
-
OK so throwing my idea in. Two changes:
With those in place, I think we can achieve a syntax that looks like (copying your example): await the_rule_to_call()
await the_rule_to_call(request=Arg1())
await the_rule_to_call(arg1=Arg1(), arg2=Arg2()) # This was previously the dictionary calling syntax (I think we ought to set a semi-strict convention on request parameter names, but that relates to #18905 (comment) in that we have a one-time chance to rename things) |
Beta Was this translation helpful? Give feedback.
-
Another thing to remember here is that callers of rules must now import them, so moving them around breaks the plugin API where it did not before. Worth it, but not "no hardship". Also, does this potentially mean not needing to register rules in register.py, if all rules are explicitly imported anyway? |
Beta Was this translation helpful? Give feedback.
-
As referenced on #19053, we should be making a lot more memoization decisions than we currently are.
That's lightly relevant to this issue, in that this issue changes memoized-by-default call sites to look a lot more like "rule helper" callsites (which are not). Another potential connection is that in many cases, we wrap in a return type for the purposes of call-by-type callsites: removing them by using call-by-name instead would reduce memory overhead. It's probably still the case that memoization should be disabled by the callee rather than the caller though. |
Beta Was this translation helpful? Give feedback.
-
Thanks folks: have opened #19730 to cover implementation. |
Beta Was this translation helpful? Give feedback.
-
Reopening, because we might also need a new While: await MultiGet(
one_rule(2, "one", **implicit()),
two_rule("two", **implicit()),
) ... might be made to typecheck, |
Beta Was this translation helpful? Give feedback.
-
#19797 now contains two or three example usages of the The name of this function needs to be:
Scala 3 has renamed "implicit parameters" to "contextual parameters" (i.e. "parameters that come from the context"), which is definitely an improvement. But Scala declares contextual parameters in the callee rather than at the caller, so the equivalent might be: |
Beta Was this translation helpful? Give feedback.
-
I've realized I don't know how this affects union registration and use. Currently we register a union of concrete request types and iterate over them via UnionMembership:
What is the call-by-name-equivalent? |
Beta Was this translation helpful? Give feedback.
-
@stuhood Can we close this discussion as resolved as we're in the implementation phase? |
Beta Was this translation helpful? Give feedback.
-
Just to clear things out: with this new design it won't be possible to hook into the existing backend rule anymore, right? Here is what I mean. Imagine you have a backend, let's say docker backend, and you want some custom logic in one of the rules that docker backend provides. Right now what I can do is I can create a new plugin, import all rules from the docker backend except for the one I want to customize, and declare my own new rule with the same signature. This way I can reuse all docker backend logic and customize a single rule. But with call-by-name I won't be able to do it, right? |
Beta Was this translation helpful? Give feedback.
-
tl;dr: This issue proposes introducing a syntax to call
@rule
s by name, and deprecating theGet
-by-return-type syntax in almost all cases (@union
s TBD).Motivation
Currently, rather than actually referring to the
@rule
that will be called,Get
s use a return-type-centric syntax (await Get(SomeReturnType, SomeArgument(..))
). But as our set of plugins has expanded and our understanding of@union
s has matured, it's becoming clearer that one of the motivations for that syntax is probably no longer valid."
@rule
graph solving" (documented here) refers to the process of:@rule
implementations to use for:@rule
@rule
'sGet
sParam
s which must be part of the runtime graphNode
for a@rule
, because they will be used (transitively) to compute its dependenciesThat second aspect of
@rule
graph solving (determining the set ofParam
s that a@rule
needs in order to run) is something which cannot practically be done by hand, and so it continues to be an essential component of composing@rule
s. But the first aspect (deciding which@rule
s will be called) can definitely be done by hand, since at a fundamental level, it can be reduced to the same sort of function calling convention that is used in most programming languages: calling a function by name.There is no hardship involved in calling functions by name, but when the v2 engine was originally created, particular focus was paid to pluggability and extensibility. Avoiding calling functions by name meant that the implementations of your dependencies could be swapped out without mocking, and that "abstract"
@rule
s (currently represented as@union
s) would be on equal footing with other calls.But it's become clear in the intervening time that explicitly introducing and tracking pluggability upstream via a set of declared
@union
s is more maintainable and easier to reason about than attempting to swap out@rule
s. And the cost/complexity of rule graph solving means that:@rule
graph error messages remain incomprehensiblepantsd
is noticeable (with enough@rule
s installed, even in--release
mode)@rule
graph solving (e.g. Re-enable concurrent runs for pantsd in v2 #7654 (comment))To simplify and remove roughly half of the magic involved in rule graph solving, we should deprecate the
Get
-by-return-type syntax in favor of calling@rule
s by name (@union
s TBD).Syntax
One syntax for accomplishing this would be to adjust the
@rule
decorator to convert the function it is applied to into aCallable
with adapted arguments, which returns aGet
-shaped type. For a@rule
like:Callsites could allow positional arguments and keyword arguments, which would be typed. To use the existing relevant portion of
Get
syntax to provide any (transitive) parameters for the@rule
call, they would use a new**implicitly()
function/syntax (which "reads" similar to what it does: fill in the remaining arguments to the@rule
):Beta Was this translation helpful? Give feedback.
All reactions