Skip to content

OpMethod support#23

Merged
hinerm merged 17 commits into
masterfrom
scijava/scijava-ops/op-methods
Jan 19, 2021
Merged

OpMethod support#23
hinerm merged 17 commits into
masterfrom
scijava/scijava-ops/op-methods

Conversation

@gselzer

@gselzer gselzer commented Sep 18, 2020

Copy link
Copy Markdown
Member

This PR introduces support for writing ops as methods. Ops written as methods are discovered using the @OpMethod annotation, which declares both the name of the Op as well as the type of the Op (this allows for extensibility). Method ops allow the declaration of OpDependencies, which are included within the parameters accepted by the method.

Method ops might look as follows:

@OpMethod(names = "stats.mean", type = java.util.function.Function.class)
public Double meanOp(
  Iterable<Number> data, 
  @OpDependency(name = "stats.sum") Function<Iterable<Number>, Double> sumOp, 
  @OpDependency(name = "stats.size") Function<Iterable<Number>, Double> sizeOp) 
  {
    Double sum = sumOp.apply(data);
    Double size =  sizeOp.apply(data);
    return sum / size;
  }

This Op calculates the mean (hence the name stats.mean), and is a Function<Iterable<Number>, Double> (as declared by type and the set of parameters in the method). This Op also has two OpDependencies: one Function<Iterable<Number>, Double> sumOp and one Function<Iterable<Number>, Double> sizeOp. There are (should be 😁) no restrictions on the position of the OpDependencies within the method signature.

The op would be called as follows:

Iterable<Number> data = ...
Double mean = ops.op("stats.mean").input(data).outType(Double.class).apply();

Both OpDependencies will automagically be injected by the OpService using Javassist. It is important to note that this PR does introduce a dependency on Javassist, which does not yet have a module. When a method op has no OpDependencies, it will be constructed instead using LambdaMetaFactory; this method is faster than using Javassist since we do not have to incur the compilation process during runtime.

This PR also introduces a suite of tests, ensuring that method Ops can be written for all of our supported Op types (Computer, Inplace, Function) both with and without OpDependencies.

TODO:

  • Decide whether we should proceed without a declared module name for Javassist. It would be nice to have one, but is probably not necessary.

Closes scijava/scijava#23.

Thanks to @MarcelWiedenmann for starting this work.

@gselzer gselzer requested review from ctrueden and hinerm September 18, 2020 17:57
@hinerm

hinerm commented Sep 21, 2020

Copy link
Copy Markdown
Member

@gselzer is this something that needs to be merged before @ctrueden comes back?

@gselzer

gselzer commented Sep 21, 2020

Copy link
Copy Markdown
Member Author

@hinerm no, it is not urgent. I only pinged you to:

  • Ensure this PR is not forgotten about
  • Get a (third) opinion on the syntax for method Ops. As someone who is not particularly in the loop about scijava-ops, do you think this syntax could be improved?

@hinerm

hinerm commented Sep 21, 2020

Copy link
Copy Markdown
Member

@gselzer for me, this seems unfortunately long:
@OpDependency(name = "stats.sum") Function<Iterable<Number>, Double> sumOp

If there was a way to do something like

@OpMethod(names = "stats.mean", type = java.util.function.Function.class, dependencies = {"stats.sum", "stats.size"})
public Double meanOp(

I think I might find that more pleasant, although I assume there's not a nice way to inject the parameters then.

Side note: what are the advantages of @OpMethod versus just taking OpEnvironment as a parameter, or giving all Ops implicit access to their environment? e.g. something like:

@OpMethod(names = "stats.mean", type = java.util.function.Function.class)
public Double meanOp(Iterable<Number> data) 
  {
    Double sum = ops().op("stats.sum").input(data).outType(Double.class).apply();
    Double size =  ops().op("stats.size").input(data).outType(Double.class).apply();
    return sum / size;
  }

Another side note: am I correct that in OpBuilder the op name doesn't actually get used until apply is called? If so it might be nice if the API allowed reusing the input and/or output configuration for multiple ops. Something like:

OpConfig conf = ops.op().input(data).outType(double.class);
Double sum = conf.apply("stats.sum");
Double size = conf.apply("stats.size");

@gselzer

gselzer commented Sep 21, 2020

Copy link
Copy Markdown
Member Author

Thanks for the feedback @hinerm. I will try to address each of your points:

If there was a way to do something like

@OpMethod(names = "stats.mean", type = java.util.function.Function.class, dependencies = {"stats.sum", "stats.size"})
public Double meanOp(

I agree that it is long, but it is also specific. We obviously cannot get shorter than Function<Iterable<Number>, Double> sumOp since that is the type and name of the parameter. This is already pretty long.

Furthermore, we need to be able to determine whether a given parameter is an Op. We cannot assume to know that if a parameter is of type X, Y, or Z then it must be an OpDependency; this would not be extensible. This is compounded by the fact that many ops (such as the project Op ) take functions as parameters that are not OpDependencies. This behavior should be available for method-based Ops as well.

If you agree with what I said above (and I would be thrilled if you found a way around this), we must also include the @OpDependency annotation to those parameters, which will also require recording the name.

Side note: what are the advantages of @OpMethod versus just taking OpEnvironment as a parameter, or giving all Ops implicit access to their environment?

The main benefit is that it provides independence from scijava-ops. If we wrote something along the lines of what you wrote above, we would then require anyone wanting to use our OpMethod to also require scijava-ops. @ctrueden has long been aware of people who do not want to go through the matching system, and this will allow them to do it. Of course, this requires them to find (write) their own sum and size ops in our example, so it is not particularly convenient to call them without Ops, but at least it can be done.

Another side note: am I correct that in OpBuilder the op name doesn't actually get used until apply is called? If so it might be nice if the API allowed reusing the input and/or output configuration for multiple ops.

You are correct in that the op name is not used until apply/compute/function/computer/... is called. I agree that the construction is nice, however I would not want to use it for anything other than Functions; Computers would not make sense under this construction since you would be overwriting the output again, and Inplaces could get really confusing really fast because you would have to keep track of which inputs were modified at which point. I am not sure how we would implement this for only Functions, thus I am hesitant to implement something along these lines. It might be worth further discussion, though...

@hinerm hinerm merged commit b57a28f into master Jan 19, 2021
@hinerm hinerm deleted the scijava/scijava-ops/op-methods branch January 19, 2021 15:19
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.

Add Op factory methods

3 participants