Skip to content
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

feature: new query architecture with support for Java8 queries #1018

Merged
merged 36 commits into from
Dec 21, 2016

Conversation

pvojtechovsky
Copy link
Collaborator

@pvojtechovsky pvojtechovsky commented Nov 30, 2016

Requirements for a new query architecture:

  • map functions with arbitrary return types (incl. that are not CtElement)
  • support for queries with Java 8 lambdas
  • chained queries ( "chaining" means writing filterChildren(x).filterChildren(y)... or map(z).map(t)... or even filterChildren(p).map(q)...)
  • lazy evaluation

With this PR you can write code like

CtMethod<?> method = ...
List<CtMethod<?>> overridenMethods = Query.getElements(new OverriddenMethodFilter(method));

Which makes usage of complex filters more reliable, because these filters can automatically define correct searching context. For example

  • OverriddenMethodFilter will search in root package
  • local variable reference filter will search in scope of its CtBock element
  • member variable reference filter will search in scope of its Class and depending on visibility in all inherited class, in current package or in root package
  • ...

or you can write

CtMethod<?> method = ...
List<CtMethod<?>> overridenMethods = method.getElements(new OverriddenMethodFilter())

which 1) is shortest code and 2) which will let us chain filters (later) into complex filters chains, which are able to browse spoon model in sequence of searching steps. I will probably need something like this for refactoring methods, I am preparing.

@tdurieux
Copy link
Collaborator

Nice. 👍

One day we will able to write modern code that uses spoon :D

@monperrus
Copy link
Collaborator

Excellent indeed! Could you add a presentation of this new feature in doc/filter.md?

@pvojtechovsky
Copy link
Collaborator Author

documentation

ok, but I would first like to finish real chaining of filters using a new QueryBuilder and PR #1017. The combination of these two things will start to be interesting. I will then document it together.

@pvojtechovsky pvojtechovsky force-pushed the searchScopeAwareFilter branch from 606b470 to 5679c8d Compare December 1, 2016 19:49
@pvojtechovsky
Copy link
Collaborator Author

Documentation added.

@tdurieux
Copy link
Collaborator

tdurieux commented Dec 1, 2016

Documentation added.

Thanks

Do you know that you can use java 8 features in the tests?

@pvojtechovsky
Copy link
Collaborator Author

java 8 features in the tests

yes, I found it when I added spoon into Eclipse. The spoon code is java 7 and tests are java 8 compatible

@pvojtechovsky
Copy link
Collaborator Author

I have found one problem of this PR. It is not backward compatible with existing filters. For example
this code of FilterTest.testFieldAccessFilter():

CtFieldReference<?> ref = ...
List<CtFieldAccess<?>> expressions = someClass.getElements(new FieldAccessFilter(ref));

will behave different when we improve FieldAccessFilter by way it implements ScopeAwareFilter and ChainableFilter. Because then the correct call is:

CtFieldReference<?> ref = ...
List<CtFieldAccess<?>> expressions = ref.getElements(new FieldAccessFilter());

How to solve that?
A) to not use CtElement#getElements, but new one like CtElement#query or searchFor or ...?
B) to keep old filters as they are and to implement new filters ... 👎
C) ? any ideas ?

By the way, Travis failed. Should I change something?

@pvojtechovsky
Copy link
Collaborator Author

I have rolled back CtElement#getElements, because it is not backward compatible.

I would actually prefer solution like this:

CtFieldReference<?> ref = ...
List<CtFieldAccess<?>> expressions = ref.search(new FieldAccessFilter()).list();
//or optionally this should be supported too
ref.search(new FieldAccessFilter()).forEach(CtFieldAccess<?>fa->{...});

Note that CtElement#search(Filter) method would return new QueryBuilder object, which can be used to build even more complex queries using fluent API. When query is built, then there are two ways how to start the query
A) QueryBuilder#list() which processes query and returns List of found elements
B) QueryBuilder#forEach(ElementConsumer consumer) which processes query and calls consumer for each found element.

If you like this approach, then it is question how to make a PR for that?
A) add QueryBuilder into this PR
B) you will merge this PR as it is and I will make new PR with QueryBuilder

@pvojtechovsky
Copy link
Collaborator Author

I have added QueryBuilder to this PR. The QueryBuilder is now used in Query API too, so it is tested by all test code which uses Queries.

The documentation does not contain QueryBuilder yet. I will update it probably tomorrow.

This old code

CtFieldReference<?> ref = ...
List<CtFieldAccess<?>> expressions = someClass.getElements(new FieldAccessFilter(ref));

can be written this way:

CtFieldReference<?> ref = ...
List<CtFieldAccess<?>> expressions = ref.query(new FieldAccessFilter()).list();

after FieldAccessFilter will implement ChainableFilter in future PRs.

@pvojtechovsky
Copy link
Collaborator Author

This new code needs further refactoring and comments, test, etc. But the idea is like this:
Query builder connects ElementPipes into chain, which can be used process several queries on the model and to collect several data from these nodes. See this example

	class Context {
		CtMethod method;
		CtMethod overridenMethod;
	}
		
	Context context = new Context();

	launcher.getFactory().Package().getRootPackage().query(new TypeFilter(CtMethod.class))
		.then(new ElementPipeImpl<CtMethod,CtMethod>(){
			@Override
			protected void onAccept(CtMethod element) {
				context.method = element;
			}
		}).then(new OverriddenMethodFilter())
		.then(new ElementPipeImpl<CtMethod,CtMethod>(){
			@Override
			protected void onAccept(CtMethod element) {
				context.overridenMethod = element;
                                ...
			}
		}).list();

it searches for all methods and then for all overriden methods of found methods. The Context class is used just as example how client can collect data from the middle of the query processing.

@pvojtechovsky pvojtechovsky force-pushed the searchScopeAwareFilter branch from 5c4d19c to 80c7dc1 Compare December 4, 2016 21:29
@pvojtechovsky
Copy link
Collaborator Author

This PR now contains support for query step chain. I implemented it again since beginning, so please forget olders concepts and check if they are still used in this PR or not.

See the first draft of documentation in doc/Filter.md.

The code is not finished yet.

  • it needs to be commented
  • I don't like CtElement#query() but how to do it better?
  • write jUnit test - the code is really not tested yet

Please review the names of the new Classes, packages and methods. Let me know if something is not understandable or confusing. Do you have a better name for AsyncFunction?
Are the QueryStep build methods then, scan and matches understandable?
Any other feedback is welcome too.

@pvojtechovsky pvojtechovsky changed the title filters may define searching context and can be used by CtElement.getElements Query can be built from steps where the optionally used filters may influence scanning context Dec 5, 2016
@pvojtechovsky
Copy link
Collaborator Author

I am still not finished, but query like this works now. Do you know what it does??

		launcher.getFactory().Package().getRootPackage().query().scan((CtClass<?> c)->{return true;})
			.then((CtClass<?> c)->c.getSuperInterfaces())
			.then((CtTypeReference<?> iface)->iface.getTypeDeclaration())
			.then((CtType<?> iface)->iface.getAllMethods())
			.then((CtMethod<?> method)->method.getSimpleName().equals("make"))
			.then((CtMethod<?> m)->m.getType())
			.then((CtTypeReference<?> t)->t.getTypeDeclaration())
			.forEach((CtInterface<?> c)->{
				assertEquals("ITostada", c.getSimpleName());
			});

It scans AST from root package, accepts CtClasses, then it navigates to all super interfaces of found classes and searches there for each method whose name is equal to "make". If an method is found then it takes its return type and checks that it has found expected value.

I have removed QueryStep#matches. It actually uses then(Function) and the QueryStep detects that returned value is Boolean and in such case behaves as predicate.

Copy link
Collaborator

@monperrus monperrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks very promising, but the Javadoc is too scarce to 100% grasp the design (one line per class describing the responsibility and key collaborating classes -- callers, callees, hierarchy -- is usually enough is single responsibility is applied).

* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*/
public interface AsyncFunction<T, R> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name and javadoc not clear enough to convey the core design intuition and contract

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why async? executed asynchronously? or do you mean deferred?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that Async is not good name for that. Deferred has a time meaning too, but it is better then Async. The purpose of this function is to produce zero, one or more elements from one input element. May be names like

  • ChainableFunction
  • CallbackFunction
  • ProductFunction
  • ...
    I like no one :-(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChainableFunction is better IMHO


import spoon.support.util.SafeInvoker;

public class AsyncFunctionQueryStep<O> extends QueryStep<O> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

javadoc?

* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*/
public interface Function<T, R> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clear

/**
* @param <T> the type of the input to the function
*/
public interface Predicate<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the same as Filter? (but the generic T?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes generic.

  1. QueryStep chain can be used to process elements which are not inherited from CtElement. I am not sure whether we needs that.
  2. Spoon Filter has already old meaning like Filtering of children produced by CtScanner, I wanted something more generic, what can be used to decide whether input element should be sent to output or not.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, could you put this explanation in the Javadoc?

* @param <I> - input type
* @param <O> - output type
*/
public abstract class QueryStep<O> implements Consumer<Object> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about having an interface QueryStep? (in Spoon, the design is very interface-oriented: most key concepts and key methods are defined in interfaces). The method then seems indeed essential for the design.

import spoon.reflect.visitor.chain.AsyncFunction;
import spoon.reflect.visitor.chain.Consumer;

public class Scann extends CtScanner implements AsyncFunction<CtElement, CtElement> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does Scann mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be used in the QueryStep chain to visit all the children of chain input element.
It is used automatically In combination with filter in method QuertStep#scan(Filter), but I guess it can be use independent using ...query().then(new Scann())....

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and why this strange name?

*/
public static Class<?>[] getMethodParameterTypes(Class<?> clazz, String methodName, int numParams) {
public static Method getMethod(Class<?> clazz, String methodName, int numParams) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non backward-compatible change on public method

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method getMethodParameterTypes is there only few days. I guess nobody is using yet. And because nobody needs it, I removed it to minimize amount of code.
But if you like this method, then I will put it back. No problem

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK if it's a recent one.

import spoon.SpoonException;

/**
* @param <T> the Type of the delegate object, which is target of method invocation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contract? responsibility?

@@ -332,6 +335,8 @@ public void testOverriddenMethodFromAbstractClass() throws Exception {
final CtClass<AbstractTostada> aClass = launcher.getFactory().Class().get(AbstractTostada.class);

assertEquals(0, Query.getElements(launcher.getFactory(), new OverriddenMethodFilter(aClass.getMethodsByName("prepare").get(0))).size());
assertEquals(0, Query.query().then(new OverriddenMethodFilter()).list(aClass.getMethodsByName("prepare").get(0)).size());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard to understand to start with a call to "then" with no element.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query.query().then(...)

yes, I do not like it too. But it was minimum code at the beginning. If you have better idea, then tell me please. Otherwise I suggest to wait little bit until I (or somebody else) start to use it and come with real experience and needs... I am also looking for a better way ...

QueryStep<CtClass<?>> l_qv = launcher.getFactory().getModel().getRootPackage().query().scan(new TypeFilter<>(CtClass.class));

assertEquals(0, context.counter);
l_qv.forEach(cls->{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the foreach very much

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to see foreach more often in Spoon API - even in core model ... I will make other PR. But later ...


launcher.getFactory().Package().getRootPackage().query().scan(new TypeFilter<CtMethod<?>>(CtMethod.class))
.thenConsume((CtMethod<?> method) -> {context.method = method;})
.then(new OverriddenMethodFilter())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what you call then is a functional map (https://en.wikipedia.org/wiki/Map_(higher-order_function))?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I need suggestions and links like that! I do not have theoretic training in these things

Context context = new Context();

launcher.getFactory().Package().getRootPackage().query().scan(new TypeFilter<CtMethod<?>>(CtMethod.class))
.thenConsume((CtMethod<?> method) -> {context.method = method;})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between then and thenConsume?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we must visibly distinguish between
A) then(...a function which produces elements for next step...)
B) thenConsume(... a function which just eats elements and produces nothing....)

We cannot let this distinction on the java compiler, because it would be confusing. Example:
(element)->element.doSomething()
Is that Consumer, which produces nothing or is it a function which produces result? Sometime the client wants to call function which returns values as consumer too.

If you write code like
...query().then(X).then(Y).then(Z)
then it processes X and the result of X is sent to Y and result of Y is sent to Z

But if you write code like
...query().then(X).thenConsume(Y).then(Z)
then it processes X and the result of X is sent to Y, which produces nothing and result of X is sent to Z

I am again open to change function name to anything better. May be I should learn some RxJava API and take over the names from that. Is there anybody who knows that API and can suggest names which are generally used and understandable?

@monperrus
Copy link
Collaborator

monperrus commented Dec 6, 2016 via email

@pvojtechovsky
Copy link
Collaborator Author

I will replace then by map.

thenConsume(Consumer) ... That's quite counter-intuitive.to me. Do we need/want this behavior?

I wanted that to be able to collect the intermediate results of the query (model browsing). But it can be replaced by

query().then((element)->{
   ...do required consuming...
  return element;
})

so we have one query "command" (thenConsume) less => simpler API and it is easy to get same behavior with existing then (map) command. I will remove it. Thanks for feedback! I want to have it simple and intuitive too.

thenConsume ... forEach (or "iter")?

thenConsume(Consumer) was intended for use in the middle of the query chain. The QueryStep#forEach(Consumer) is already there. The difference to thenConsume(Consumer) is that forEach executes the query, while thenConsume only add a QueryStep, but do not execute the query.

forEach or iter

I prefer forEach, because there is java 8 Iterable#forEach(Consumer). So I think this name will be intuitive for Java 8 developers. Or do you think it is something else and we should use different name?

@pvojtechovsky pvojtechovsky force-pushed the searchScopeAwareFilter branch from 17ffb07 to c191d5d Compare December 7, 2016 21:19
@pvojtechovsky
Copy link
Collaborator Author

pvojtechovsky commented Dec 7, 2016

... still not ready for merge. I have pushed it here to show the current state and to see whether it pass the tests. Feedback is welcome, of course.
I am mainly not sure with new CtElement methods map(...) and scan(Filter), which replaces ugly query().

@monperrus
Copy link
Collaborator

monperrus commented Dec 8, 2016 via email

@pvojtechovsky pvojtechovsky force-pushed the searchScopeAwareFilter branch from c191d5d to 10abd98 Compare December 8, 2016 21:22
@pvojtechovsky
Copy link
Collaborator Author

I have commented all the new code and did final simplifications and renames. I am satisfied with that version, but if you have any suggestions, or I forgot something then let me know please.

The test cases are still missing. Please make a review of new API so I can finally document it in Filter.md. I will do that probably on Saturday evening.

@@ -35,7 +36,7 @@
* element).
*/
@Root
public interface CtElement extends FactoryAccessor, CtVisitable, Cloneable {
public interface CtElement extends FactoryAccessor, CtVisitable, Cloneable, QueryComposer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryComposer contains all the methods, which can begin with creation of some query. It is the replacement for CtElement#query(). With QueryComposer you can easily start to query spoon model starting from any element.
Note: QueryComposer is implemented by QueryStep too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's first agree on this. I'm not fan on this because you cannot really say that a CtElement is a QueryComposer. However, I understand the idea of having an interface here because you want to have a starting point from any element, and the methods could also be used for chaining.
So we have to find a better name that grasps the responsibility. What do you think of QueryStartingPoint? (a CtElement is a query starting point).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I fully agree with you. New name QueryStartingPoint is good for CtElement. But it is not so good for QueryStep, which will inherit it too and will use these methods for building of next query step.
What about
class CtElement extends QueryStartingPoint
class QueryStartingPoint extends QueryComposer
class QuerStep extends QueryComposer

... it is a kins of compromise. The best would be to found a name which is good for both interfaces.

One related idea... There is Query class, which has actualy static methods. It would be may be nice to change them to instance methods and to let Query extend QueryComposer too. So the name of the interface should fit to that idea too. But let's duscuss it later. This PR is complex enough...

* It is implemented 1) by {@link CtElement} to allow creation new query and its first step
* 2) by {@link QueryStep} to allow creation of next query step
*/
public interface QueryComposer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the name, what do you think of QueryResult?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is there to compose first or next queryStep. It is really not about result/evaluation of the query.

If you do not like QueryComposer, then alternatives might be

  • QueryCreator
  • QueryStepBuilder
  • QueryFactory
  • ... ?

*
* @param <O> the type of the element produced by this QueryStep
*/
public interface QueryStep<O> extends QueryComposer, Consumer<Object> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The role and methods of QueryStep and QueryComposer seem close. Does it make sense to merge them in a single interface?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no. QueryComposer has only methods, which can create query step. But QueryStep has methods which can evaluate query too. It makes no sense to evaluate query on CtElement (which implements QueryComposer) directly, because there is no query yet. First you have to create one or more query steps (using QueryComposer) and then you can evaluate the query using methods, which are only on QueryStep only.

* @param <R> the type of the result produced by this function
*/
public interface ChainableFunction<T, R> {
void apply(T input, Consumer<R> output);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about extending Function (intuitive for ChainableFunction), and put the consumer as return type: Consumer<R> apply(T input?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not possible. It is different contract. Have a look at new class
src/main/java/spoon/reflect/visitor/filter/Scann.java
It implements ChainableFunction. How you would implement such thing using Consumer<R> apply(T input) ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at Scann, basically Consumer is the implicit Function. What if Consumer extends Function (accept becomes apply) and then you only have one method map?

* @param queryStep
* @return the entered queryStep, which is now the last step of the query
*/
<R> QueryStep<R> map(QueryStep<R> queryStep);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all map functions are really close (same name, same return type). It suggests a common abstraction (and the proximity of Function and ChainableFunction goes in the same direction).

Is it possible to have a single map function with an appropriate common interface as parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have a single map function with an appropriate common interface as parameter?

I do not know how it would look like. Please describe your idea in more details.

Why three functions are problem?

I need all three functions for these cases:

  1. map(QueryStep) - it let's us to combine query chains together

  2. map(Function) - it let's us to make a query based on standard functions of CtModel elements. It simply gets the input element and returned value is sent to next step. See javadoc description for details about 4 possible usages.

  3. map(ChainableFunction) it let's us implement clever Filters, which automatically computes the scanning scope. For example FieldReferencesFilter, which searches for references to an field depending on field visibility.

Note: we cannot merge map(Function) and map(ChainableFunction) into one. Because Function#apply has 1 input parameter, while ChainableFunction#apply has 2 parameters. And if I would make one Interface, which would contains both apply functions, then it would be not possible to implement it using java 8 lambda expression, because it expects only one non default method in the interface.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why three functions are problem?

because it's not elegant and hints to a design issue.

@monperrus
Copy link
Collaborator

added the requirements for this new feature at the top of this thread.

@pvojtechovsky
Copy link
Collaborator Author

Thanks for Your PR! It helped me to understand Your thinking. Now I can try to clarify the misunderstanding, which is there. You are trying to implement different feature then me.
My PR contains these features:
F1 possibility to implement CQS like FieldReferencesQuery. I need that for another PR about refactoring.
F2 chaining of query steps
F3 the query steps based on CtFunction
Your PR contains only F2 and F3. It is not sufficient for me. OK, I know that you preffer small steps and my PR was too big step.

So I will comment your PR now and then merge it.

@pvojtechovsky
Copy link
Collaborator Author

Please see my comments in your PR, but do not fix anything in your PR. I will takeover it as it is and then, I will simplify it even more. So we will have first small step on which we both can agree on.

What I will do?
I like the idea of FieldReferenceFilter as Query. So we will have no query steps. Just queries which can be composed from other queries.
I will try to give a chance the idea of replacing Consumer by CtFunction<T,Void>
I will remove CtQueryable from the CtElement. The problem of that approach is that Query has predefined input (source element) since beginning and it makes things more complicated then is needed. So after that change we will evaluate query on an inputElementthis way:

factory.Query().map(new FieldReferenceQuery()).forEach(resultConsumingFunction, inputElement
//OR
List list=factory.Query().map(new FieldReferenceQuery()).list(inputElement)
//OR
factory.Query().map(new FieldReferenceQuery()).map(resultConsumingFunction).list(inputElement)

This approach is not so nice like current one, but it is simplest code and the most powerful and flexible way. Later we can use that low level code and to write some nicer facade about that, so the usability will improve too.
I am also not sure about factory.Query, forEach and list method name... etc.

One more idea: If we will not merge code of OverridenMethodFilter with OverridenMethodQuery, then we can even merge map function with filterChildren, because the algoritm and client will know what to do by looking at implemented interface or class name. But may be filterChildren is good name, which intuitivelly explains what such query step will do, so we can keep it.

@monperrus
Copy link
Collaborator

I will takeover it as it is and then, I will simplify it even more. So we will have first small step on which we both can agree on.

OK, excellent.

Just queries which can be composed from other queries.

Sounds a cool idea

But may be filterChildren is good name, which intuitivelly explains what such query step will do, so we can keep it.

The big advantage of filterChildren right now, is that it's a natural explanation and transition step from old-style filters no new-style queries.

I will remove CtQueryable from the CtElement.

Are you sure? It's a really meaningful, concise and coherent interface, perfectly fitting into CtElement.

@pvojtechovsky
Copy link
Collaborator Author

I will remove CtQueryable from the CtElement.

Are you sure? It's a really meaningful, concise and coherent interface, perfectly fitting into CtElement.

I agree with you, but it is kind of facade over the internal query engine. And I have learned from this PR, that too much steps at once causes misunderstanding. So I want to have first low level query engine merged in Spoon and then we can discuss how to integrate it into CtElement.
Why that? The interface like CtQueryStep#forEach(Consumer out, CtElement input) is the low level and I NEED it to build other features. But if I directly implement CtElement facade, then it looks like this interface is not needed and You tends to delete it. ;-)

@monperrus
Copy link
Collaborator

monperrus commented Dec 19, 2016 via email

@pvojtechovsky
Copy link
Collaborator Author

I am not able to implement next version, because each idea ends with the progress killing thought: "Martin doesn't like CtConsumer, CtQueryStep, forEach"....
... and I do not like list(), which will need a lot of memory for queries in my project where I need to process 6000 classes * 20 methods * 5 parameter * ?? elements * in queries with about 5 to 10 query steps.
... and which produces not nice code like:

List list = ...query.list();
for(item:list) {
 ... do something with each item
}

spoon is already full of such code. Just search for usages of CtElement#getElements()
So I am giving up. Any idea how to proceed?

@pvojtechovsky
Copy link
Collaborator Author

Here is the exact definition what avoids future progress from my side:
Your PR on my PR deletes OverriddenMethodFilter#forEach(CtConsumer<CtMethod> output, CtMethod input), which is the example of usage of MAIN feature of my PR. With code of your PR it is impossible for me to implement such method effectively. Show me how to implement it using Your approach. Then
A) I will understand a way, which is evident for You, but not for me.
B) You will understand that CtQueryStep#forEach(Consumer, input) and CtConsumer are good and will not try to remove it from the design.

@monperrus
Copy link
Collaborator

monperrus commented Dec 20, 2016 via email

@pvojtechovsky
Copy link
Collaborator Author

I merged your PR.

What prevents this to your opinion?

Query composition is not possible now with your PR.

For implementing scope-based query, we will probably either add new
methods in the query interfaces or new interfaces,

ok, I suggest that You continue on future query development, until it gets at least to state, where it is possible to implement scope based queries.

but again, let's first agree on the core design here.

this core design is quite limited now. I agree with anything what will cause a progress with this PR. But I will spent my effort on another Spoon features now, which are based on the state of this PR before your PR was merged.

@monperrus monperrus changed the title feature: new query architecture with fine-grain control and query composition feature: new query architecture with support for Java8 queries Dec 20, 2016
@monperrus
Copy link
Collaborator

monperrus commented Dec 20, 2016 via email

@pvojtechovsky
Copy link
Collaborator Author

pvojtechovsky commented Dec 20, 2016

map functions with arbitrary return types (incl. that are not CtElement)

yes

chained queries

partially. You can chain implementations of CtFunction, but you cannot chain queries.

lazy evaluation

partially. The CtFunction is evaluated lazily for each input element, but it is not possible the apply lazy evaluation to output of that function, because function must return results using return statement and cannot call next query step by Consumer.apply(element). Therefore usage of such queries is limited to smaller result sets.

support for queries with Java 8 lambdas

yes

But I have no problem if you merge it like it is and then continue with scope-based query.

And before merge, I suggest you fix the little problems in your PR. See my comments in your PR.

@monperrus
Copy link
Collaborator

monperrus commented Dec 20, 2016

But I have no problem if you merge it like it

The idea is to be 100% on the same wavelength before merging

chained queries

partially. You can chain implementations of CtFunction, but you cannot chain queries.

to me, "chaining" means writing filterChildren(x).filterChildren(y)... or map(z).map(t)... or even filterChildren(p).map(q)...

This is completely supported, right?

@pvojtechovsky
Copy link
Collaborator Author

yes this is supported

@monperrus
Copy link
Collaborator

On lazy evaluationn you say

The CtFunction is evaluated lazily for each input element, but it is not possible the apply lazy evaluation to output of that function, because function must return results using return statement and cannot call next query step by Consumer.apply(element). Therefore usage of such queries is limited to smaller result sets.

I don't understand. Could you elaborate?

To me, in the current implementation, all queries are only performed by calling list() on the last query method. Correct?

@pvojtechovsky
Copy link
Collaborator Author

pvojtechovsky commented Dec 20, 2016

it is not possible the apply lazy evaluation to output of that function

I don't understand. Could you elaborate?

The implementation of ChildrenFilteringWrapper is correct, because it calls

if (matches) {
	//send input to output, because Fitler.matches returned true
	next.accept(element);
} 

So it will work effectively for large amount of elements too.

The implementation of FunctionWrapper is correct too, because CtFunction is intended for query steps, which will return relatively small number of results.

So what is wrong? There is no way how to extend Query by way it works well for functions, which produces large result set. You mentioned it will be next step. OK. But as I mentioned until this feature is back again, I cannot continue, so I will continue on my old branch in the mean time.

To me, in the current implementation, all queries are only performed by calling list() on the last query method. Correct?

Correct.

Anyway thanks for Your effort Martin, to try to make it/keep it understandable!

@monperrus
Copy link
Collaborator

@surli this PR is now ready for your review. there is no need to read and understand the whole discussion, the PR itself (doc, javadoc, tests) should now be self-contained.

@@ -42,8 +46,8 @@ public QueryVisitor(Filter<T> filter) {
if (filter instanceof AbstractFilter) {
filteredType = ((AbstractFilter<T>) filter).getType();
} else {
Class<?>[] params = RtHelper.getMethodParameterTypes(filter.getClass(), "matches", 1);
filteredType = (Class<T>) params[0];
Method method = RtHelper.getMethod(filter.getClass(), "matches", 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You did not check here that method can be null as you did in AbstractFilter: it's on purpose because you assume it can never happens or is it missing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot imagine situation when "matches" method is missing, because Filter interface has such method.
But I can add check there. Thank you

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}).list();
assertTrue(context.count==1);
assertTrue(context.totalCount>1);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about what you're testing there, compared to the previous test.
Why don't you use a NameFilter here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right this test brings nothing new, The testFunctionQueryStep already tests the same. I will remove this test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have deleted the test

*
* @param &lt;T> - the type of accepted elements
*/
private interface CtConsumer<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose exactly to distinguish this interface from Step? From my point of view, each time you use a CtConsumer it's in fact a Step you're using, am I wrong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need CtConsumer as public interface later. But Martin made it private in this first step, because after his simplifications/deleting of functionality this interface is not needed.

@surli surli merged commit adaa3f5 into INRIA:master Dec 21, 2016
@pvojtechovsky pvojtechovsky deleted the searchScopeAwareFilter branch January 21, 2017 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants