Skip to content

Conversation

@gselzer
Copy link
Member

@gselzer gselzer commented Jun 4, 2021

This PR introduces the ability to declare optional parameters in an Op's functional method using the @Optional annotation. The motivation behind this implementation was to maintain from ImageJ Ops the ability to declare optional parameters without the complexity of field injection.

Once one parameter (or more) has been declared optional for some Op, scijava ops will create a set of ReducedOpInfos for that Op; if an Op has n optional parameters, we will then have n+1 OpInfos that delegate to that Op (n of which being ReducedOpInfos). These ReducedOpInfos can then be matched, just as any other OpInfo. We maintain ImageJ Ops' behavior of matching subsets of optional parameters in a left-to-right manner; this means that if there is an OpMethod

public Double foo(Double a, @Optional Double b, @Optional Double c) {}

we can match this Op using:

  • a
  • a and b
  • a, b, and c.

We do not allow the matching of a and c, as when b and c are the same type, passing a Double would provide no indication whether that Double should be assigned to b or c. To be deterministic we assume that optional parameters are always left off right to left.

Using this paradigm, users can write a class Op with optional parameters as

@Plugin(type = Op.class, name = "foo")
public  class OptionalArgClass implements BiFunction<Double, Double, Double> {
  public Double apply( Double in1, @Optional in2) {
    if (in2 == null) in2 = <reasonable default>;
    ...
  }
}

When this Op is called without the optional parameter, the ReducedOpInfo will use Javassist to create a wrapper for OptionalArgClass, passing null for each optional parameter that is omitted in the matched ReducedOpInfo. note that the Op author is then responsible for null-checking any optional arguments.

Using the OpBuilder syntax, this Op can then be called using either

Double d1 = 1.0;
Double d2 = 2.0;
Double o = ops.op("foo").input(d1, d2).outType(Double.class).apply();

or

Double d1 = 1.0;
Double o = ops.op("foo").input(d1).outType(Double.class).apply();

Ops written as methods can similarly be written with optional parameters:

@OpMethod(names = "bar", type = BiFunction.class)
public Double barMethod(Double in1, @Optional Double in2) {
  if (in2 == null) in2 = <reasonable default>;
  ...
}

When writing an Op as a Field, there are two options. One can write the Op as either an anonymous Class:

@OpField(names = "foobar")
public final BiFunction<Double, Double, Double> foobar = new BiFunction<> {
  public Double apply( Double in1, @Optional in2) {
    if (in2 == null) in2 = <reasonable default>;
    ...
  }
}

or, with the help of a specialized interface, using a lambda:

public interface BiFunctionWithOptional<I1, I2, I3, O> extends Functions.Arity3<I1, I2, I3, O> {
  @Override  
  public O apply(I1 in1, I2 in2, @Optional I3 in3);
}

@OpField(names = "baz")
public final BiFunctionWithOptional<Double, Double, Double, Double> baz = (in1, in2, in3) -> {
  if (in3 == null) in3 = <reasonable default>;
  ...
};

To promote extensibility, each OpInfo is reduced using a InfoReducer plugin. InfoReducers are designed to be able to reduce OpInfos backed by one of a set of functional types (for example, a FunctionReducer should reduce all Functions). For each functional type an InfoReducer can reduce, it should be able to produce an OpInfo for any number of optional parameters. So if, for example, an InfoReducer is able to reduce a Functions.Arity3, it should be able to reduce that Arity3 into a BiFunction, a Function, and a Producer. OpInfo reduction is performed at plugin discovery time, and the Javassist wrapper is created when the Op is matched.

Closes scijava/scijava#14

gselzer added 5 commits May 25, 2021 13:50
These methods tell us about the presence of Optional parameters on the
Op
We want to leave open the possibility for @optional annotations to be
declared on the interface as well as on the Op implementation. This
means we have to check multiple Methods for the Optional annotation.
This could be a source of bugs, but for now we check any method with the
same name as the functional method, having the same number of
parameters.
@gselzer gselzer force-pushed the scijava/scijava-ops/optional-parameters branch 3 times, most recently from 63fa3a2 to c075c6f Compare June 4, 2021 18:25
gselzer added 19 commits June 4, 2021 13:27
Field reductions will be very rare, since you cannot annotate lambda
parameters, but it is not too difficult to add support for Field Ops
that are anonymous classes
This was really confusing, now it is less so :)
In writing this test I found that getMethods works differntly than I
expected. I knew that there were two instances of the functional method
that were being returned by Class.getMethods(). I did not realize that
both were from the class (I had thought that one came from the
implemented functional interface). It turns out that both come from the
class, so we need to do some extra introspection on the functional
interface
Unfortuantely, this results in some API changes in OpEnvironment.
This makes it easier to parallelize later
Not only IS it a functional interface, but our findFunctionalInterface
logic will delegate to Supplier otherwise (which is bad)
We need the names to correctly implement the interfaces/call the
original method
These tests show that reduction with dependencies works regardless of
where the Dependencies are in the signature, which is uber cool!
This check was never necessary
@gselzer gselzer force-pushed the scijava/scijava-ops/optional-parameters branch from c075c6f to 7d1d7d4 Compare June 4, 2021 19:01
@elect86
Copy link

elect86 commented Nov 10, 2021

What about default parameters in Kotlin?

@gselzer
Copy link
Member Author

gselzer commented Nov 10, 2021

What about default parameters in Kotlin?

How does that help us, unless we convert our entire codebase to Kotlin?

Tangentially, I suppose that, if one wrote an Op in Kotlin, we could have a KotlinOpInfo, and then a ReducedKotlinOpInfo or something that knows how to create an Op from a chunk of Kotlin. I think that could coexist with our current Java code? Of course, you'd probably want to house that in some SciJava Ops Kotlin module or something...

gselzer added a commit that referenced this pull request Mar 24, 2022
This commit ports the work from
#32, updating it to the latest
schemes of discovery.

Please see that PR for a summary of how reduction operates
@gselzer gselzer mentioned this pull request Mar 24, 2022
@ctrueden
Copy link
Member

Closing in favor of #58.

@ctrueden ctrueden closed this Oct 31, 2022
@ctrueden ctrueden deleted the scijava/scijava-ops/optional-parameters branch October 31, 2022 20:09
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.

Allow primary parameters to be optional

4 participants