-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Make all Nodes immutable but still easy to "modify" #1335
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
Conversation
add NodePosition to general Traverser AstTransformer WIP
renamed ChildContainer to NodeChildContainer general named children in Traverser
|
There still might be some improvements, but I expect that all major parts for that change are in place: Every Node is now immutable. It introduces the concept of a Zipper: basically a pointer to a specific Node in a immutable Tree which also keeps track of where where the current Node is in respect to the parent. (see here ) Some background details: the problem with an immutable Ast is, that it is very easy to change a Node and all the children, but is very hard then also to change the parents referencing the node you just changed, because a Node just knows about its children, nothing about it parent. Zipper is a general concept which helps with that. I can recommend http://learnyouahaskell.com/zippers as a more detailed explanation. I also took inspiration in implementing it from this book. (Warning: it actually talks about Haskell and not Java) Additionally there is the class AstMultiZipper which is a list of Zippers and allows keeping track of multiple changes in a Tree at the same time.
It will look like this:
Feedback welcome. Also: Now that every Node is immutable you can't change a query anymore at any point. If you have some use cases which requires changing the query after it was create, please comment here. We wanna offer specific hooks where a query can be modified. |
|
Ping @tinnou for feedback. thanks |
| } | ||
| ExecutionResultNode newNode = parent.withNewChildren(newChildren); | ||
| return new ExecutionResultZipper(newNode, restBreadcrumbs); | ||
| } |
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.
TODO ^
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.
done
| private Map<ExecutionResultNode, List<ExecutionResultZipper>> zipperWithSameParent(List<ExecutionResultZipper> zippers) { | ||
| //todo: change to LinkedHashMap | ||
| return zippers.stream().collect(Collectors.groupingBy(ExecutionResultZipper::getParent)); | ||
| } |
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.
TODO ^
I think you put in a supplier of the Map
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.
done
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.
Yeah I reckon create its own helper method
| curZippers.removeAll(deepestZippers); | ||
| curZippers.addAll(newZippers); | ||
| } | ||
| assertTrue(curZippers.size() == 1, "illegal state"); |
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.
better assertion message - eg state the precondition better or have no message
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.
done
| private final String name; | ||
| private final List<Argument> arguments = new ArrayList<>(); | ||
|
|
||
| private static final String CHILD_ARGUMENTS = "arguments"; |
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.
not sure on the CHILD_ smurfing
The thing that represents "arguments" could be called arguments
| throw new IllegalArgumentException("Cannot pass non-empty newChildren to Node that doesn't hold children"); | ||
| } | ||
|
|
||
| return this; |
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.
This one does not seem to be finished.
Is this a BUG?
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.
well: you can't add children to a FloatValue, so we only allow empty children here
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.
I would create a helper method for these checks
@Override
public FloatValue withNewChildren(NodeChildrenContainer newChildren) {
NodeChildrenContainer.assertEmpty(newChildren);
return this;
}
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.
done via helper method
| } | ||
|
|
||
| return this; | ||
| } |
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.
Yeah - defintely common method if this is needed
@Override
public NullValue withNewChildren(NodeChildrenContainer newChildren) {
return NodeChildrenContainer.assertNotEmpty(newChildren);
}
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.
refactored it
| /** | ||
| * PRIVATE: Used by {@link Traverser} | ||
| */ | ||
| void setCurAccValue(Object curAccValue) { |
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.
bad Javadoc - its going to lead to warnings and set of my OCD on having a clean command line compile / build
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.
changed it no java doc
| } | ||
| NodePosition that = (NodePosition) o; | ||
| return index == that.index && | ||
| Objects.equals(name, that.name); |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
| def midA = rootField.selectionSet.children[0] as Field | ||
| def leafA = midA.selectionSet.children[0] as Field | ||
| def leafB = midA.selectionSet.children[0] as Field | ||
|
|
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.
I think the indices here are wrong.
def leafB = midA.selectionSet.children[1] as Field
here and below
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.
fixed
|
|
||
| def midB = rootField.selectionSet.children[1] as Field | ||
| def leafC = midB.selectionSet.children[0] as Field | ||
| def leafD = midB.selectionSet.children[0] as Field |
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.
indices are wrong I think
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.
fixed
| return NodeChildrenContainer.newNodeChildrenContainer() | ||
| .child(CHILD_VALUE, value) | ||
| .build(); | ||
| } |
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.
I would do static import in all the places to reduce the boiler plate
@Override
public NodeChildrenContainer getNamedChildren() {
return newNodeChildrenContainer()
.child(CHILD_VALUE, value)
.build();
}
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.
done
| import java.util.Objects; | ||
|
|
||
| @PublicApi | ||
| public class AstBreadcrumb { |
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.
I woukld still love to see JavaDoc at the top to outline the class.
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.
added
| import static java.util.stream.Collectors.toList; | ||
|
|
||
| @PublicApi | ||
| public class AstMultiZipper { |
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.
JavaDoc
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.
added
| @@ -0,0 +1,85 @@ | |||
| package graphql.language; | |||
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.
Should all this transformation code be in graphql.language or one down in say graphql.language.transformation ?
I think the latter
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.
AstZipper and AstMultiZipper is actually not coupled to transformations: it is a general concept.
the only two transformation classes are AstTransformer and AstTransformerUtil. Extra package for them?
| } | ||
|
|
||
| public AstZipper moveUp() { | ||
| return null; |
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.
is this not done???
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.
and hence not tested??
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.
I removed all not used methods for now.
|
|
||
| @Override | ||
| public IntValue withNewChildren(NodeChildrenContainer newChildren) { | ||
| assertNewChildrenAreEmpty(newChildren); |
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.
Are you have one. Do it on FLoatValue
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.
done ... I forgot FloatValue somehow
| */ | ||
| List<Node> getChildren(); | ||
|
|
||
| NodeChildrenContainer getNamedChildren(); |
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.
JavaDoc
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.
added
| import static graphql.Assert.assertNotNull; | ||
|
|
||
| @PublicApi | ||
| public class NodeChildrenContainer { |
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.
JavaDoc?
Maybe a seperate PR for the JavaDoc say
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.
added
| leave: { TraverserContext context -> | ||
| TraversalControl.CONTINUE | ||
| } | ||
| ] as TraverserVisitor |
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.
what is this crazy syntax?? I didnt know you could do this! :)
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.
a groovy map with closures/functions as value ... not so crazy, or?
AstMultiZipper fix for changing the structure in multiple nodes more AstTransformer tests
Perhaps I'm too late, but I'm aware of users that allow queries for objects without a subselection, by modifying the query on the fly to include all the direct fields in such cases. This was done via an instrumentation. |
|
Ah, just seen #1345 which caters exactly to this use-case :) |
The goal of this PR to make all Nodes immutable, but "modifying" the Ast should be still easy to.
This means that you can easily get a new tree when you modify a Node, even if the Node is deep inside the tree.
contains contribution from @felipe-gdr :-)