diff --git a/imagej/imagej-ops2/src/main/java/module-info.java b/imagej/imagej-ops2/src/main/java/module-info.java index 248aa56e7..5c9599563 100644 --- a/imagej/imagej-ops2/src/main/java/module-info.java +++ b/imagej/imagej-ops2/src/main/java/module-info.java @@ -1,6 +1,7 @@ module net.imagej.ops2 { // -- Open plugins to scijava-common + opens net.imagej.ops2; opens net.imagej.ops2.coloc to org.scijava, org.scijava.ops; opens net.imagej.ops2.coloc.icq to org.scijava, org.scijava.ops; opens net.imagej.ops2.coloc.kendallTau to org.scijava, org.scijava.ops; diff --git a/scijava/scijava-ops/src/main/java/module-info.java b/scijava/scijava-ops/src/main/java/module-info.java index 221e79455..7eb9f4dfb 100644 --- a/scijava/scijava-ops/src/main/java/module-info.java +++ b/scijava/scijava-ops/src/main/java/module-info.java @@ -16,6 +16,7 @@ // -- Open plugins to scijava-common opens org.scijava.ops to org.scijava; opens org.scijava.ops.impl to org.scijava; + opens org.scijava.ops.reduce to org.scijava; // FIXME: This is a file name and is thus unstable requires geantyref; diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpEnvironment.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpEnvironment.java index e52e31c10..8251a0cce 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpEnvironment.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpEnvironment.java @@ -120,23 +120,24 @@ default OpBuilder op(final String opName) { * Creates an {@link OpInfo} from an {@link Class}. * * @param opClass + * @param names the comma-delimited set of names under which this Op is known * @return an {@link OpInfo} which can make instances of {@code opClass} */ - OpInfo opify(Class opClass); + OpInfo opify(Class opClass, String names); /** * Creates an {@link OpInfo} from an {@link Class} with the given priority. * * @param opClass + * @param names the comma-delimited set of names under which this Op is known * @param priority - the assigned priority of the Op. * @return an {@link OpInfo} which can make instances of {@code opClass} */ - OpInfo opify(Class opClass, double priority); + OpInfo opify(Class opClass, String names, double priority); /** - * Makes the {@link OpInfo} {@code info} known to this {@link OpEnvironment} under the name {@code name} + * Makes the {@link OpInfo} {@code info} known to this {@link OpEnvironment} * @param info - * @param name */ - void register(OpInfo info, String name); + void register(OpInfo info); } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpInfo.java index bca5d202b..cd499b38e 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpInfo.java @@ -2,6 +2,7 @@ package org.scijava.ops; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.List; @@ -29,7 +30,10 @@ public interface OpInfo extends Comparable { /** Gets the associated {@link Struct} metadata. */ Struct struct(); - + + /** Gets the comma-delimited set of names used to identify the Op */ + String names(); + /** Describes whether this Op can be simplified. */ boolean isSimplifiable(); @@ -67,6 +71,8 @@ default OpCandidate createCandidate(OpEnvironment env, Logger log, OpRef ref, Ma AnnotatedElement getAnnotationBearer(); + boolean isOptional(Member m); + @Override default int compareTo(final OpInfo that) { if (this.priority() < that.priority()) return 1; diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpUtils.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpUtils.java index 32df09bfa..362f38cfa 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/OpUtils.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/OpUtils.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import org.scijava.ops.matcher.MatchingResult; @@ -136,6 +137,15 @@ public static Type[] inputTypes(Struct struct) { return getTypes(inputs(struct)); } + public static Class[] inputRawTypes(Struct struct) { + Type[] inputTypes = getTypes(inputs(struct)); + Class[] inputRawTypes = new Class[inputTypes.length]; + for (int i = 0; i < inputTypes.length; i++) { + inputRawTypes[i] = Types.raw(inputTypes[i]); + } + return inputRawTypes; + } + public static Member output(OpCandidate candidate) { return candidate.opInfo().output(); } @@ -150,6 +160,10 @@ public static List> outputs(final Struct struct) { .collect(Collectors.toList()); } + public static Type outputType(final Struct candidate) { + return outputs(candidate).get(0).getType(); + } + public static List> outputs(StructInstance op) { return op.members().stream() // .filter(memberInstance -> memberInstance.member().isOutput()) // @@ -435,4 +449,21 @@ public static Class findFirstImplementedFunctionalInterface(final OpRef opRef } return null; } + + /** + * Returns the index of the argument that is both the input and the output. If there is no such argument (i.e. the Op produces a pure output), -1 is returned + * + * @return the index of the mutable argument. + */ + public static int ioArgIndex(final OpInfo info) { + List> inputs = OpUtils.inputs(info.struct()); + Optional> ioArg = inputs.stream().filter(m -> m.isInput() && m.isOutput()).findFirst(); + if(ioArg.isEmpty()) return -1; + Member ioMember = ioArg.get(); + return inputs.indexOf(ioMember); + } + + public static boolean hasPureOutput(final OpInfo info) { + return ioArgIndex(info) == -1; + } } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Functions.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Functions.java index 0b5e0aa49..29b38618b 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Functions.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Functions.java @@ -78,7 +78,7 @@ private Functions() { * @throws NullPointerException If {@code type} is {@code null}. */ public static boolean isFunction(Type type) { - return ALL_FUNCTIONS.containsKey(Types.raw(type)); + return ALL_FUNCTIONS.containsValue(Types.raw(type)); } @SuppressWarnings({ "unchecked" }) diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Producer.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Producer.java index d91f9b28d..2edfe98c3 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Producer.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/function/Producer.java @@ -18,6 +18,7 @@ * @param * The type of objects produced. */ +@FunctionalInterface public interface Producer extends Supplier { O create(); diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/impl/DefaultOpEnvironment.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/impl/DefaultOpEnvironment.java index f1d244f81..a518f56f2 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/impl/DefaultOpEnvironment.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/impl/DefaultOpEnvironment.java @@ -45,6 +45,8 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -78,6 +80,7 @@ import org.scijava.ops.matcher.OpMatchingException; import org.scijava.ops.matcher.OpMethodInfo; import org.scijava.ops.matcher.OpRef; +import org.scijava.ops.reduce.InfoReducer; import org.scijava.ops.simplify.InfoSimplificationGenerator; import org.scijava.ops.simplify.SimplificationUtils; import org.scijava.ops.simplify.SimplifiedOpInfo; @@ -136,6 +139,11 @@ public class DefaultOpEnvironment extends AbstractContextual implements OpEnviro */ private Map, OpWrapper> wrappers; + /** + * Data structure storing all discoverable {@link InfoReducer}s. + */ + private List infoReducers; + public DefaultOpEnvironment(final Context context) { context.inject(this); matcher = new DefaultOpMatcher(log); @@ -189,19 +197,19 @@ public T bakeLambdaType(final T op, Type reifiedType) { } @Override - public OpInfo opify(final Class opClass) { - return opify(opClass, Priority.NORMAL); + public OpInfo opify(final Class opClass, String names) { + return opify(opClass, names, Priority.NORMAL); } @Override - public OpInfo opify(final Class opClass, final double priority) { - return new OpClassInfo(opClass, priority, opClass.getAnnotation(Unsimplifiable.class) == null); + public OpInfo opify(final Class opClass, final String names, final double priority) { + return new OpClassInfo(opClass, names, priority, opClass.getAnnotation(Unsimplifiable.class) == null); } @Override - public void register(final OpInfo info, final String name) { + public void register(final OpInfo info) { if (opDirectory == null) initOpDirectory(); - addToOpIndex(info, name); + addToOpIndex(info); } @SuppressWarnings("unchecked") @@ -475,6 +483,10 @@ private void initWrappers() { } } + private void initInfoReducers() { + infoReducers = pluginService.createInstancesOfType(InfoReducer.class); + } + /** * Attempts to inject {@link OpDependency} annotated fields of the specified * object by looking for Ops matching the field type and the name specified in @@ -662,46 +674,92 @@ private OpRef inferOpRef(Type type, String name, Map, Type> type return new OpRef(name, type, mappedOutputs[0], mappedInputs); } + /** + * Initializes the Op Directory. There are two phases: + *
    + *
  1. Ops written as Classes are discovered + *
  2. Ops written as Fields and Methods are discovered via {@link OpCollection} annotations + *
+ * + * TODO: can this be done in parallel? + */ private void initOpDirectory() { opDirectory = new HashMap<>(); + initInfoReducers(); // Add regular Ops - for (final PluginInfo pluginInfo : pluginService.getPluginsOfType(Op.class)) { - try { - final Class opClass = pluginInfo.loadClass(); - OpInfo opInfo = new OpClassInfo(opClass); - addToOpIndex(opInfo, pluginInfo.getName()); - } catch (InstantiableException exc) { - log.error("Can't load class from plugin info: " + pluginInfo.toString(), exc); - } - } + pluginService.getPluginsOfType(Op.class) // + .stream().forEach(parseOpClass); // Add Ops contained in an OpCollection - for (final PluginInfo pluginInfo : pluginService.getPluginsOfType(OpCollection.class)) { - try { - final Class c = pluginInfo.loadClass(); - final List fields = ClassUtils.getAnnotatedFields(c, OpField.class); - Object instance = null; - for (Field field : fields) { - final boolean isStatic = Modifier.isStatic(field.getModifiers()); - if (!isStatic && instance == null) { - instance = field.getDeclaringClass().newInstance(); - } - OpInfo opInfo = new OpFieldInfo(isStatic ? null : instance, field); - addToOpIndex(opInfo, field.getAnnotation(OpField.class).names()); - } - final List methods = ClassUtils.getAnnotatedMethods(c, OpMethod.class); - for (final Method method: methods) { - OpInfo opInfo = new OpMethodInfo(method); - addToOpIndex(opInfo, method.getAnnotation(OpMethod.class).names()); + pluginService.getPluginsOfType(OpCollection.class) // + .stream().forEach(parseOpCollection); + } + + private final Consumer reduceInfo = (info) -> { + // if there is nothing to reduce, end quickly + boolean hasOptional = OpUtils.inputs(info.struct()).parallelStream() // + .anyMatch(m -> info.isOptional(m)); // + if (!hasOptional) return; + + // find a InfoReducer capable of reducing info + Optional suitableReducer = infoReducers + .parallelStream().filter(reducer -> reducer.canReduce(info)).findAny(); + if (suitableReducer.isEmpty()) { + log.warn("Cannot reduce " + info + ": No suitable InfoReducer!"); + return; + } + + InfoReducer reducer = suitableReducer.get(); + long numReductions = info.struct().members().parallelStream() // + .filter(m -> info.isOptional(m)) // + .count(); // + // add a ReducedOpInfo for all possible reductions + // TODO: how to find the names? + for (int i = 1; i <= numReductions; i++) { + addToOpIndex(reducer.reduce(info, i)); + } + }; + + private final Consumer> parseOpClass = (pluginInfo) -> { + try { + final Class opClass = pluginInfo.loadClass(); + OpInfo opInfo = new OpClassInfo(opClass, pluginInfo.getName()); + addToOpIndex(opInfo); + reduceInfo.accept(opInfo); + } catch (InstantiableException exc) { + log.error("Can't load class from plugin info: " + pluginInfo.toString(), exc); + } + }; + + private final Consumer> parseOpCollection = pluginInfo -> { + try { + final Class c = pluginInfo.loadClass(); + final List fields = ClassUtils.getAnnotatedFields(c, OpField.class); + Object instance = null; + for (Field field : fields) { + final boolean isStatic = Modifier.isStatic(field.getModifiers()); + if (!isStatic && instance == null) { + instance = field.getDeclaringClass().newInstance(); } - } catch (InstantiableException | InstantiationException | IllegalAccessException exc) { - log.error("Can't load class from plugin info: " + pluginInfo.toString(), exc); + String names = field.getAnnotation(OpField.class).names(); + OpInfo opInfo = new OpFieldInfo(isStatic ? null : instance, field, names); + addToOpIndex(opInfo); + reduceInfo.accept(opInfo); + } + final List methods = ClassUtils.getAnnotatedMethods(c, OpMethod.class); + for (final Method method: methods) { + String names = method.getAnnotation(OpMethod.class).names(); + OpInfo opInfo = new OpMethodInfo(method, names); + addToOpIndex(opInfo); + reduceInfo.accept(opInfo); } + } catch (InstantiableException | InstantiationException | IllegalAccessException exc) { + log.error("Can't load class from plugin info: " + pluginInfo.toString(), exc); } - } + }; - private void addToOpIndex(final OpInfo opInfo, final String opNames) { - String[] parsedOpNames = OpUtils.parseOpNames(opNames); + private void addToOpIndex(final OpInfo opInfo) { + String[] parsedOpNames = OpUtils.parseOpNames(opInfo.names()); if (parsedOpNames == null || parsedOpNames.length == 0) { log.error("Skipping Op " + opInfo.implementationName() + ":\n" + "Op implementation must provide name."); return; diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpAdaptationInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpAdaptationInfo.java index 5f630559e..efaa87e31 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpAdaptationInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpAdaptationInfo.java @@ -8,8 +8,10 @@ import org.scijava.ops.OpDependencyMember; import org.scijava.ops.OpInfo; import org.scijava.ops.OpUtils; +import org.scijava.param.Optional; import org.scijava.param.ParameterStructs; import org.scijava.param.ValidityException; +import org.scijava.struct.Member; import org.scijava.struct.Struct; import org.scijava.struct.StructInstance; @@ -58,6 +60,11 @@ public Struct struct() { return struct; } + @Override + public String names() { + return srcInfo.names(); + } + // we want the original op to have priority over this one. @Override public double priority() { @@ -105,4 +112,16 @@ public boolean isSimplifiable() { return false; } + /** + * NB for {@link Optional} annotations to be on the Op, they would have to be + * declared within the adapter. Since this is unlikely (and is probably bad + * practice), we will assume that they do not exist. + */ + @Override + public boolean isOptional(Member m) { + if (!struct.members().contains(m)) throw new IllegalArgumentException( + "Member " + m + " is not a Memeber of OpInfo " + this); + return false; + } + } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpClassInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpClassInfo.java index 8160da329..aebfb9f53 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpClassInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpClassInfo.java @@ -32,17 +32,24 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.List; import org.scijava.Priority; import org.scijava.ops.OpDependencyMember; import org.scijava.ops.OpInfo; import org.scijava.ops.OpUtils; +import org.scijava.ops.reduce.ReductionUtils; import org.scijava.ops.simplify.Unsimplifiable; +import org.scijava.param.Optional; import org.scijava.param.ParameterStructs; import org.scijava.param.ValidityException; +import org.scijava.param.ValidityProblem; import org.scijava.plugin.Plugin; +import org.scijava.struct.Member; import org.scijava.struct.Struct; import org.scijava.struct.StructInstance; import org.scijava.types.Types; @@ -57,25 +64,42 @@ public class OpClassInfo implements OpInfo { private final Class opClass; private Struct struct; + private final String names; private ValidityException validityException; private final double priority; - + + private final Boolean[] paramOptionality; private final boolean simplifiable; - public OpClassInfo(final Class opClass) { - this(opClass, priorityFromAnnotation(opClass), simplifiableFromAnnotation(opClass)); + public OpClassInfo(final Class opClass, final String names) { + this(opClass, names, priorityFromAnnotation(opClass), simplifiableFromAnnotation( + opClass)); } - public OpClassInfo(final Class opClass, final double priority, final boolean simplifiable) { + public OpClassInfo(final Class opClass, final String names, final double priority, + final boolean simplifiable) + { + final List problems = new ArrayList<>(); + this.opClass = opClass; + this.names = names; try { struct = ParameterStructs.structOf(opClass); OpUtils.checkHasSingleOutput(struct); - } catch (ValidityException e) { - validityException = e; - } + } + catch (ValidityException e) { + problems.addAll(e.problems()); + } this.priority = priority; this.simplifiable = simplifiable; + + // determine parameter optionality + paramOptionality = getParameterOptionality(opClass, + struct, problems); + + + validityException = problems.isEmpty() ? null : new ValidityException( + problems); } // -- OpInfo methods -- @@ -84,7 +108,7 @@ public OpClassInfo(final Class opClass, final double priority, final boolean public Type opType() { // TODO: Check whether this is correct! return Types.parameterizeRaw(opClass); - //return opClass; + // return opClass; } @Override @@ -92,6 +116,11 @@ public Struct struct() { return struct; } + @Override + public String names() { + return names; + } + @Override public double priority() { return priority; @@ -146,23 +175,22 @@ public StructInstance createOpInstance(List dependencies) { public ValidityException getValidityException() { return validityException; } - + @Override public boolean isValid() { return validityException == null; } - + @Override public AnnotatedElement getAnnotationBearer() { return opClass; } - + // -- Object methods -- @Override public boolean equals(final Object o) { - if (!(o instanceof OpClassInfo)) - return false; + if (!(o instanceof OpClassInfo)) return false; final OpInfo that = (OpInfo) o; return struct().equals(that.struct()); } @@ -185,7 +213,8 @@ private static double priorityFromAnnotation(Class annotationBearer) { } private static boolean simplifiableFromAnnotation(Class annotationBearer) { - final Unsimplifiable opAnnotation = annotationBearer.getAnnotation(Unsimplifiable.class); + final Unsimplifiable opAnnotation = annotationBearer.getAnnotation( + Unsimplifiable.class); return opAnnotation == null ? true : false; } @@ -193,4 +222,57 @@ private static boolean simplifiableFromAnnotation(Class annotationBearer) { public boolean isSimplifiable() { return simplifiable; } + + /** + * TODO: this implementation seems hacky, for two reasons. + *
    + *
  1. We are assuming that the Struct's Members and their corresponding + * {@link Parameter}s on the functional Method have the same ordering. There + * is no real guarantee that this is the case.
  2. + *
  3. There doesn't seem to be a good way to reliably determine where the + * {@link Optional} annotations might be. Ideally, they'd be on the functional + * method overriden within the class itself. But sometimes (this happens in + * ImageJ Ops2's geom package) the functional method is overriden within a + * superclass of the class we have, and to reduce code duplication we'd like + * to support that too. It could also be useful to put the annotation directly + * on the method of the Functional Interface (suppose you write a + * {@code BiFunctionWithOptional} interface)
  4. + *
+ */ + @Override + public boolean isOptional(Member m) { + if (!struct.members().contains(m)) throw new IllegalArgumentException( + "Member " + m + " is not a Memeber of OpInfo " + this); + if (m.isOutput()) return false; + if (m instanceof OpDependencyMember) return false; + int inputIndex = OpUtils.inputs(struct).indexOf(m); + // TODO: call this method once? + return paramOptionality[inputIndex]; + } + + private static Boolean[] getParameterOptionality(Class opType, + Struct struct, List problems) + { + List fMethodsWithOptionals = ReductionUtils.fMethodsWithOptional(opType); + Class fIface = ParameterStructs.findFunctionalInterface(opType); + List fIfaceMethodsWithOptionals = ReductionUtils.fMethodsWithOptional(fIface); + // the number of parameters we need to determine + int opParams = OpUtils.inputs(struct).size(); + if (fMethodsWithOptionals.isEmpty() && fIfaceMethodsWithOptionals.isEmpty()) { + return ReductionUtils.generateAllRequiredArray(opParams); + } + if (!fMethodsWithOptionals.isEmpty() && !fIfaceMethodsWithOptionals.isEmpty()) { + problems.add(new ValidityProblem( + "Multiple methods from the op type have optional parameters!")); + return ReductionUtils.generateAllRequiredArray(opParams); + } + if (fMethodsWithOptionals.isEmpty()) { + return ReductionUtils.findParameterOptionality(fIfaceMethodsWithOptionals.get(0)); + } + if (fIfaceMethodsWithOptionals.isEmpty()) { + return ReductionUtils.findParameterOptionality(fMethodsWithOptionals.get(0)); + } + return ReductionUtils.generateAllRequiredArray(opParams); + } + } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpFieldInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpFieldInfo.java index aa64ec92a..bffdf7f03 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpFieldInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpFieldInfo.java @@ -31,6 +31,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; @@ -40,10 +41,12 @@ import org.scijava.ops.OpField; import org.scijava.ops.OpInfo; import org.scijava.ops.OpUtils; +import org.scijava.ops.reduce.ReductionUtils; import org.scijava.ops.simplify.Unsimplifiable; import org.scijava.param.ParameterStructs; import org.scijava.param.ValidityException; import org.scijava.param.ValidityProblem; +import org.scijava.struct.Member; import org.scijava.struct.Struct; import org.scijava.struct.StructInstance; @@ -56,15 +59,18 @@ public class OpFieldInfo implements OpInfo { private final Object instance; private final Field field; + private final String names; private Struct struct; private ValidityException validityException; + private final Boolean[] paramOptionality; private final boolean simplifiable; - public OpFieldInfo(final Object instance, final Field field) { + public OpFieldInfo(final Object instance, final Field field, final String names) { this.instance = instance; this.field = field; + this.names = names; if (Modifier.isStatic(field.getModifiers())) { // Field is static; instance must be null. @@ -100,6 +106,10 @@ public OpFieldInfo(final Object instance, final Field field) { // we cannot simplify the Op iff it has the Unsimplifiable annotation. simplifiable = field.getAnnotation(Unsimplifiable.class) == null; + + // determine parameter optionality + paramOptionality = getParameterOptionality(instance, field, + struct, problems); } // -- OpInfo methods -- @@ -115,6 +125,11 @@ public Struct struct() { return struct; } + @Override + public String names() { + return names; + } + @Override public double priority() { final OpField opField = field.getAnnotation(OpField.class); @@ -185,4 +200,51 @@ public String toString() { public boolean isSimplifiable() { return simplifiable; } + + @Override + public boolean isOptional(Member m) { + if (!struct.members().contains(m)) throw new IllegalArgumentException( + "Member " + m + " is not a Memeber of OpInfo " + this); + if (m.isOutput()) return false; + int inputIndex = OpUtils.inputs(struct).indexOf(m); + // TODO: call this method once? + return paramOptionality[inputIndex]; + } + + private static Boolean[] getParameterOptionality(Object instance, Field field, + Struct struct, List problems) + { + // the number of parameters we need to determine + int opParams = OpUtils.inputs(struct).size(); + + Class fieldClass; + try { + fieldClass = field.get(instance).getClass(); + } + catch (IllegalArgumentException | IllegalAccessException exc) { + // TODO Auto-generated catch block + problems.add(new ValidityProblem(exc)); + return ReductionUtils.generateAllRequiredArray(opParams); + } + List fMethodsWithOptionals = ReductionUtils.fMethodsWithOptional(fieldClass); + Class fIface = ParameterStructs.findFunctionalInterface(fieldClass); + List fIfaceMethodsWithOptionals = ReductionUtils.fMethodsWithOptional(fIface); + + if (fMethodsWithOptionals.isEmpty() && fIfaceMethodsWithOptionals.isEmpty()) { + return ReductionUtils.generateAllRequiredArray(opParams); + } + if (!fMethodsWithOptionals.isEmpty() && !fIfaceMethodsWithOptionals.isEmpty()) { + problems.add(new ValidityProblem( + "Multiple methods from the op type have optional parameters!")); + return ReductionUtils.generateAllRequiredArray(opParams); + } + if (fMethodsWithOptionals.isEmpty()) { + return ReductionUtils.findParameterOptionality(fIfaceMethodsWithOptionals.get(0)); + } + if (fIfaceMethodsWithOptionals.isEmpty()) { + return ReductionUtils.findParameterOptionality(fMethodsWithOptionals.get(0)); + } + return ReductionUtils.generateAllRequiredArray(opParams); + } + } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpMethodInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpMethodInfo.java index fc832f704..30a327185 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpMethodInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/matcher/OpMethodInfo.java @@ -48,6 +48,7 @@ import org.scijava.ops.OpInfo; import org.scijava.ops.OpMethod; import org.scijava.ops.OpUtils; +import org.scijava.ops.reduce.ReductionUtils; import org.scijava.ops.simplify.Unsimplifiable; import org.scijava.ops.util.Adapt; import org.scijava.param.ParameterStructs; @@ -76,11 +77,13 @@ public class OpMethodInfo implements OpInfo { private final Method method; private Type opType; private Struct struct; + private final String names; + private Boolean[] paramOptionality; private final ValidityException validityException; private final boolean simplifiable; - public OpMethodInfo(final Method method) { + public OpMethodInfo(final Method method, final String names) { final List problems = new ArrayList<>(); // Reject all non public methods if (!Modifier.isPublic(method.getModifiers())) { @@ -95,6 +98,7 @@ public OpMethodInfo(final Method method) { " must be static.")); } this.method = method; + this.names = names; // we cannot simplify this op iff it has the Unsimplifiable annotation. simplifiable = method.getAnnotation(Unsimplifiable.class) == null; try { @@ -111,6 +115,11 @@ public OpMethodInfo(final Method method) { catch (final ValidityException e) { problems.addAll(e.problems()); } + + // determine parameter optionality + paramOptionality = getParameterOptionality(this.method, Types.raw(opType), + struct, problems); + validityException = problems.isEmpty() ? null : new ValidityException( problems); } @@ -127,6 +136,11 @@ public Struct struct() { return struct; } + @Override + public String names() { + return names; + } + @Override public double priority() { final OpMethod opMethod = method.getAnnotation(OpMethod.class); @@ -191,8 +205,29 @@ private Object javassistOp(Method m, List dependencies) // Create wrapper class String className = formClassName(m); - CtClass cc = pool.makeClass(className); + Class c; + try { + // use javassist to create the class + CtClass cc = constructOpMethodWrapper(pool, className, m); + c = cc.toClass(MethodHandles.lookup()); + } + catch (RuntimeException e) { + // the OpMethod has already been created - find it + c= Class.forName(className); + } + // Return Op instance + List> depMembers = OpUtils.dependencies(struct()); + Class[] depClasses = depMembers.stream().map(dep -> dep.getRawType()) + .toArray(Class[]::new); + return c.getDeclaredConstructor(depClasses).newInstance(dependencies + .toArray()); + } + + private CtClass constructOpMethodWrapper(ClassPool pool, String className, + Method m) throws Throwable + { + CtClass cc = pool.makeClass(className); // Add implemented interface CtClass jasOpType = pool.get(Types.raw(opType).getName()); cc.addInterface(jasOpType); @@ -212,13 +247,7 @@ private Object javassistOp(Method m, List dependencies) // add functional interface method CtMethod functionalMethod = CtNewMethod.make(createFunctionalMethod(m), cc); cc.addMethod(functionalMethod); - - // Return Op instance - Class[] depClasses = depMembers.stream().map(dep -> dep.getRawType()) - .toArray(Class[]::new); - Class c = cc.toClass(MethodHandles.lookup()); - return c.getDeclaredConstructor(depClasses).newInstance(dependencies - .toArray()); + return cc; } private String formClassName(Method m) { @@ -359,4 +388,86 @@ public AnnotatedElement getAnnotationBearer() { public boolean isSimplifiable() { return simplifiable; } + + /** + * TODO: this implementation seems hacky. We are assuming that the Struct's + * Members and their corresponding {@link Parameter}s on the functional Method + * have the same ordering. There is no real guarantee that this is the case. + */ + @Override + public boolean isOptional(Member m) { + if (!struct.members().contains(m)) throw new IllegalArgumentException( + "Member " + m + " is not a Memeber of OpInfo " + this); + if (m.isOutput()) return false; + if (m instanceof OpDependencyMember) return false; + // TODO: this likely will break down if OpDependencies + int inputIndex = OpUtils.inputs(struct).indexOf(m); + return paramOptionality[inputIndex]; + } + + private static Boolean[] getParameterOptionality(Method m, Class opType, + Struct struct, List problems) + { + boolean opMethodHasOptionals = ReductionUtils.hasOptionalAnnotations(m); + List fMethodsWithOptionals = ReductionUtils.fMethodsWithOptional(opType); + // the number of parameters we need to determine + int opParams = OpUtils.inputs(struct).size(); + + // Ensure only the Op method OR ONE of its op type's functional methods have + // Optionals + if (opMethodHasOptionals && !fMethodsWithOptionals.isEmpty()) { + problems.add(new ValidityProblem( + "Both the OpMethod and its op type have optional parameters!")); + return ReductionUtils.generateAllRequiredArray(opParams); + } + if (fMethodsWithOptionals.size() > 1) { + problems.add(new ValidityProblem( + "Multiple methods from the op type have optional parameters!")); + return ReductionUtils.generateAllRequiredArray(opParams); + } + + // return the optionality of each parameter of the Op + if (opMethodHasOptionals) return getOpMethodOptionals(m, opParams); + if (fMethodsWithOptionals.size() > 0) return ReductionUtils.findParameterOptionality( + fMethodsWithOptionals.get(0)); + return ReductionUtils.generateAllRequiredArray(opParams); + } + + private static Boolean[] getOpMethodOptionals(Method m, int opParams) { + int[] paramIndex = mapFunctionalParamsToIndices(m.getParameters()); + Boolean[] arr = ReductionUtils.generateAllRequiredArray(opParams); + // check parameters on m + Boolean[] mOptionals = ReductionUtils.findParameterOptionality(m); + for(int i = 0; i < mOptionals.length; i++) { + int index = paramIndex[i]; + if (index == -1) continue; + arr[index] |= mOptionals[i]; + } + return arr; + } + + /** + * Since {@link OpMethod}s can have an {@link OpDependency} (or multiple) as + * parameters, we need to determine which parameter indices correspond to the + * inputs of the Op. + * + * @param parameters the list of {@link Parameter}s of the {@link OpMethod} + * @return an array of ints where the value at index {@code i} denotes the + * position of the parameter in the Op's signature. Values of + * {@code -1} designate an {@link OpDependency} at that position. + */ + private static int[] mapFunctionalParamsToIndices(Parameter[] parameters) { + int[] paramNo = new int[parameters.length]; + int paramIndex = 0; + for(int i = 0; i < parameters.length; i++) { + if (parameters[i].isAnnotationPresent(OpDependency.class)) { + paramNo[i] = -1; + } + else { + paramNo[i] = paramIndex++; + } + } + return paramNo; + } + } \ No newline at end of file diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ComputerReducer.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ComputerReducer.java new file mode 100644 index 000000000..2570dfbed --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ComputerReducer.java @@ -0,0 +1,38 @@ +package org.scijava.ops.reduce; + +import java.lang.reflect.Type; + +import org.scijava.ops.OpInfo; +import org.scijava.ops.OpUtils; +import org.scijava.ops.function.Computers; +import org.scijava.param.ParameterStructs; +import org.scijava.plugin.Plugin; +import org.scijava.util.Types; + +@Plugin(type = InfoReducer.class) +public class ComputerReducer implements InfoReducer{ + + @Override + public boolean canReduce(OpInfo info) { + return Computers.isComputer(ParameterStructs.findFunctionalInterface(Types.raw(info.opType()))); + } + + @Override + public ReducedOpInfo reduce(OpInfo info, int numReductions) { + Type opType = info.opType(); + Class rawType = ParameterStructs.findFunctionalInterface(Types.raw(opType)); + Integer originalArity = Computers.ALL_COMPUTERS.get(rawType); + Integer reducedArity = originalArity - numReductions; + Class reducedRawType = Computers.ALL_COMPUTERS.inverse().get(reducedArity); + Type[] inputTypes = OpUtils.inputTypes(info.struct()); + Type outputType = OpUtils.outputType(info.struct()); + Type[] newTypes = new Type[reducedArity + 1]; + for(int i = 0; i < reducedArity; i++) { + newTypes[i] = inputTypes[i]; + } + newTypes[newTypes.length - 1] = outputType; + Type reducedOpType = Types.parameterize(reducedRawType, newTypes); + return new ReducedOpInfo(info, reducedOpType, originalArity - reducedArity); + } + +} diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/FunctionReducer.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/FunctionReducer.java new file mode 100644 index 000000000..9752eb205 --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/FunctionReducer.java @@ -0,0 +1,38 @@ +package org.scijava.ops.reduce; + +import java.lang.reflect.Type; + +import org.scijava.ops.OpInfo; +import org.scijava.ops.OpUtils; +import org.scijava.ops.function.Functions; +import org.scijava.param.ParameterStructs; +import org.scijava.plugin.Plugin; +import org.scijava.util.Types; + +@Plugin(type = InfoReducer.class) +public class FunctionReducer implements InfoReducer{ + + @Override + public boolean canReduce(OpInfo info) { + return Functions.isFunction(ParameterStructs.findFunctionalInterface(Types.raw(info.opType()))); + } + + @Override + public ReducedOpInfo reduce(OpInfo info, int numReductions) { + Type opType = info.opType(); + Class rawType = ParameterStructs.findFunctionalInterface(Types.raw(opType)); + Integer originalArity = Functions.ALL_FUNCTIONS.inverse().get(rawType); + Integer reducedArity = originalArity - numReductions; + Class reducedRawType = Functions.ALL_FUNCTIONS.get(reducedArity); + Type[] inputTypes = OpUtils.inputTypes(info.struct()); + Type outputType = OpUtils.outputType(info.struct()); + Type[] newTypes = new Type[reducedArity + 1]; + for(int i = 0; i < reducedArity; i++) { + newTypes[i] = inputTypes[i]; + } + newTypes[newTypes.length - 1] = outputType; + Type reducedOpType = Types.parameterize(reducedRawType, newTypes); + return new ReducedOpInfo(info, reducedOpType, originalArity - reducedArity); + } + +} diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/InfoReducer.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/InfoReducer.java new file mode 100644 index 000000000..52a895c92 --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/InfoReducer.java @@ -0,0 +1,12 @@ + +package org.scijava.ops.reduce; + +import org.scijava.ops.OpInfo; +import org.scijava.plugin.SciJavaPlugin; + +public interface InfoReducer extends SciJavaPlugin { + + boolean canReduce(OpInfo info); + + ReducedOpInfo reduce(OpInfo info, int numReductions); +} diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReducedOpInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReducedOpInfo.java new file mode 100644 index 000000000..94abf5049 --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReducedOpInfo.java @@ -0,0 +1,129 @@ +package org.scijava.ops.reduce; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Objects; + +import org.scijava.ops.OpDependencyMember; +import org.scijava.ops.OpInfo; +import org.scijava.ops.OpUtils; +import org.scijava.param.Optional; +import org.scijava.param.ParameterStructs; +import org.scijava.param.ValidityException; +import org.scijava.struct.Member; +import org.scijava.struct.Struct; +import org.scijava.struct.StructInstance; + + +public class ReducedOpInfo implements OpInfo { + + private final OpInfo srcInfo; + private final Type reducedOpType; + private final int paramsReduced; + + private Struct struct; + private ValidityException validityException; + + public ReducedOpInfo(OpInfo src, Type reducedOpType, int paramsReduced) { + this.srcInfo = src; + this.reducedOpType = reducedOpType; + this.paramsReduced = paramsReduced; + + try { + this.struct = ParameterStructs.structOf(srcInfo, reducedOpType); + } + catch (ValidityException e) { + validityException = e; + } + } + + @Override + public Type opType() { + return reducedOpType; + } + + @Override + public Struct struct() { + return struct; + } + + @Override + public String names() { + return srcInfo.names(); + } + + @Override + public boolean isSimplifiable() { + return true; + } + + @Override + public double priority() { + return srcInfo.priority(); + } + + @Override + public String implementationName() { + // TODO: improve this name + return srcInfo.implementationName() + "Reduction" + paramsReduced; + } + + /** + * Gets the op's dependencies on other ops. NB the reduction wrapper has no + * dependencies, but the Op itself might. So the dependencies are actually + * reflected in {@code srcInfo} + */ + @Override + public List> dependencies() { + return OpUtils.dependencies(srcInfo().struct()); + } + + @Override + public StructInstance createOpInstance(List dependencies) { + final Object op = srcInfo.createOpInstance(dependencies).object(); + try { + Object reducedOp = ReductionUtils.javassistOp(op, this); + return struct().createInstance(reducedOp); + } + catch (Throwable ex) { + throw new IllegalArgumentException( + "Failed to invoke reduction of Op: \n" + srcInfo + + "\nProvided Op dependencies were: " + Objects.toString(dependencies), + ex); + } + } + + @Override + public boolean isValid() { + return validityException == null; + } + + @Override + public ValidityException getValidityException() { + return validityException; + } + + @Override + public AnnotatedElement getAnnotationBearer() { + return srcInfo.getAnnotationBearer(); + } + + /** + * NB since this {@link OpInfo} has already been reduced, we ignore any + * remaining {@link Optional} parameters + */ + @Override + public boolean isOptional(Member m) { + return false; + } + + public OpInfo srcInfo() { + return srcInfo; + } + + public int paramsReduced() { + return paramsReduced; + } + +} diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReductionUtils.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReductionUtils.java new file mode 100644 index 000000000..c577bc097 --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/reduce/ReductionUtils.java @@ -0,0 +1,314 @@ +package org.scijava.ops.reduce; + +import com.google.common.collect.Streams; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.scijava.ops.OpUtils; +import org.scijava.ops.simplify.SimplificationUtils; +import org.scijava.ops.simplify.Simplifier; +import org.scijava.param.Optional; +import org.scijava.param.ParameterStructs; +import org.scijava.struct.Member; +import org.scijava.types.Types; + +import javassist.CannotCompileException; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; +import javassist.CtField; +import javassist.CtMethod; +import javassist.CtNewConstructor; +import javassist.CtNewMethod; +import javassist.Modifier; +import javassist.NotFoundException; + +public class ReductionUtils { + + /** + * Creates a Class given an Op and a set of {@link Simplifier}s. This class: + *
    + *
  • is of the same functional type as the given Op + *
  • has type arguments that are of the simplified form of the type + * arguments of the given Op (these arguments are dictated by the list of + * {@code Simplifier}s. + *
  • + * + * @param originalOp - the Op that will be simplified + * @param reducedInfo - the {@link ReducedOpInfo} containing the information + * required to reduce {@code originalOp}. + * @return a wrapper of {@code originalOp} taking arguments that are then + * mutated to satisfy {@code originalOp}, producing outputs that are + * then mutated to satisfy the desired output of the wrapper. + * @throws Throwable + */ + protected static Object javassistOp(Object originalOp, ReducedOpInfo reducedInfo) throws Throwable { + ClassPool pool = ClassPool.getDefault(); + + // Create wrapper class + String className = formClassName(reducedInfo); + Class c; + try { + c = pool.getClassLoader().loadClass(className); + } + catch (ClassNotFoundException e) { + CtClass cc = generateSimplifiedWrapper(pool, className, reducedInfo); + c = cc.toClass(MethodHandles.lookup()); + } + + // Return Op instance + return c.getDeclaredConstructor(Types.raw(reducedInfo.srcInfo().opType())) + .newInstance(originalOp); + } + + /** + * A valid class name must be unique. + * @param reducedInfo + * @return the class name + */ + private static String formClassName(ReducedOpInfo reducedInfo) { + // -- package name -- + // NB required to be this package for the Lookup to work + String packageName = getPackageName(); + StringBuilder sb = new StringBuilder(packageName + "."); + + // -- class name -- + // Start with the class of the implementation + String originalName = className(reducedInfo); + // Add the input members for uniqueness + String simplifiedParameters = memberNames(reducedInfo); + + // -- ensure the name is valid -- + String className = originalName.concat(simplifiedParameters); + if(className.chars().anyMatch(c -> !Character.isJavaIdentifierPart(c))) + throw new IllegalArgumentException(className + " is not a valid class name!"); + + // -- full name is package + class -- + sb.append(className); + return sb.toString(); + } + + private static String getPackageName() { + return ReductionUtils.class.getPackageName(); + } + + private static String className(ReducedOpInfo reducedInfo) { + String implName = reducedInfo.implementationName(); + int parenIndex = implName.indexOf('('); + int classStart; + // no paren - structure is package.class + if (parenIndex == -1) { + classStart = implName.lastIndexOf('.') + 1; + } + // paren - structure is packge.class.method(params) + else { + int methodStart = implName.substring(0, parenIndex).lastIndexOf('.'); + classStart = implName.substring(0, methodStart).lastIndexOf('.') + 1; + } + + String originalName = implName.substring(classStart); // we only want the class name + // replace non-valid identifiers with underscore (the underscore is arbitrary) + return originalName.replaceAll("[^A-Z^a-z0-9$_]", "_"); + } + + private static String memberNames(ReducedOpInfo reducedInfo) { + Stream memberNames = // + Streams.concat(Arrays.stream(OpUtils.inputTypes(reducedInfo.struct())), // + Stream.of(OpUtils.outputType(reducedInfo.struct()))) // + .map(type -> getClassName(Types.raw(type))); + Iterable iterableNames = (Iterable) memberNames::iterator; + String simplifiedParameters = String.join("_", iterableNames); + return simplifiedParameters; + } + + /** + * {@link Class}es of array types return "[]" when + * {@link Class#getSimpleName()} is called. Those characters are invalid in a + * class name, so we exchange them for the suffix "_Arr". + * + * @param clazz - the {@link Class} for which we need a name + * @return - a name that is legal as part of a class name. + */ + private static String getClassName(Class clazz) { + String className = clazz.getSimpleName(); + if(className.chars().allMatch(c -> Character.isJavaIdentifierPart(c))) + return className; + if(clazz.isArray()) + return clazz.getComponentType().getSimpleName() + "_Arr"; + return className; + } + + private static CtClass generateSimplifiedWrapper(ClassPool pool, String className, ReducedOpInfo reducedInfo) throws Throwable + { + CtClass cc = pool.makeClass(className); + // Add implemented interface + CtClass jasOpType = pool.get(Types.raw(reducedInfo.opType()).getName()); + cc.addInterface(jasOpType); + + // Add Op field + CtField opField = createOpField(pool, cc, Types.raw(reducedInfo.srcInfo().opType()), "op"); + cc.addField(opField); + + // Add constructor to take the Simplifiers, as well as the original op. + CtConstructor constructor = CtNewConstructor.make(createConstructor(cc, + reducedInfo), cc); + cc.addConstructor(constructor); + + // add functional interface method + CtMethod functionalMethod = CtNewMethod.make(createFunctionalMethod(reducedInfo), + cc); + cc.addMethod(functionalMethod); + return cc; + } + + private static CtField createOpField(ClassPool pool, CtClass cc, Class opType, String fieldName) + throws NotFoundException, CannotCompileException + { + CtClass fType = pool.get(opType.getName()); + CtField f = new CtField(fType, fieldName, cc); + f.setModifiers(Modifier.PRIVATE + Modifier.FINAL); + return f; + } + + private static String createConstructor(CtClass cc, ReducedOpInfo reducedInfo) { + StringBuilder sb = new StringBuilder(); + // constructor signature + sb.append("public " + cc.getSimpleName() + "("); + // argument - original op + Class opClass = Types.raw(reducedInfo.srcInfo().opType()); + sb.append(" " + opClass.getName() + " op"); + sb.append(") {"); + + sb.append("this.op = op;"); + sb.append("}"); + return sb.toString(); + } + + /** + * Creates the functional method of a reduced Op. This functional method must: + *
      + *
    1. Call the {@code Op} using the required pure inputs, followed by + * {@code null} {@link Optional} pure arguments, followed by the i/o + * argument (iff it exists). + *
    + * NB The Javassist compiler + * does + * not fully support generics, so we must ensure that the types are raw. + * At compile time, the raw types are equivalent to the generic types, so this + * should not pose any issues. + * + * @param info - the {@link ReducedOpInfo} containing the + * information needed to write the method. + * @return a {@link String} that can be used by + * {@link CtMethod#make(String, CtClass)} to generate the functional + * method of the simplified Op + */ + private static String createFunctionalMethod(ReducedOpInfo info) { + StringBuilder sb = new StringBuilder(); + + // determine the name of the functional method + Class fIface = ParameterStructs.findFunctionalInterface(Types.raw(info.opType())); + Method m = ParameterStructs.singularAbstractMethod(fIface); + Class srcFIface = ParameterStructs.findFunctionalInterface(Types.raw(info.srcInfo().opType())); + Method srcM = ParameterStructs.singularAbstractMethod(srcFIface); + // determine the name of the output: + String opOutput = "out"; + + //-- signature -- // + sb.append(generateSignature(m)); + + //-- body --// + + // processing + sb.append(" {"); + if (OpUtils.hasPureOutput(info)) { + sb.append(OpUtils.outputType(info.struct()).getTypeName() + " " + opOutput + " = "); + } + sb.append("op." + srcM.getName() + "("); + int i; + List> totalArguments = OpUtils.inputs(info.srcInfo().struct()); + int totalArgs = OpUtils.inputs(info.srcInfo().struct()).size(); + long totalOptionals = OpUtils.inputs(info.srcInfo().struct()) + .parallelStream().filter(member -> info.srcInfo().isOptional(member)) + .count(); + long neededOptionals = totalOptionals - info.paramsReduced(); + int reducedArg = 0; + int optionals = 0; + for (i = 0; i < totalArgs; i++) { + // NB due to our optionality paradigm (if there are n optional parameters, + // they must be the last n), we just need to pass null for the last n + // arguments + if (!info.srcInfo().isOptional(totalArguments.get(i))) { + sb.append(" in" + reducedArg++); + } else if (optionals < neededOptionals) { + sb.append(" in" + reducedArg++); + optionals++; + } + else { + sb.append(" null"); + } + if (i + 1 < totalArguments.size()) sb.append(","); + } + + sb.append(");"); + + // if pure output, return it + if (OpUtils.hasPureOutput(info)) { + sb.append("return out;"); + } + sb.append("}"); + return sb.toString(); + } + + private static String generateSignature(Method m) { + StringBuilder sb = new StringBuilder(); + String methodName = m.getName(); + + // method modifiers + boolean isVoid = m.getReturnType() == void.class; + sb.append("public " + (isVoid ? "void" : "Object") + " " + methodName + + "("); + + int inputs = m.getParameterCount(); + for (int i = 0; i < inputs; i++) { + sb.append(" Object in" + i); + if (i < inputs - 1) sb.append(","); + } + + sb.append(" )"); + + return sb.toString(); + } + + public static Boolean hasOptionalAnnotations(Method m) { + return Arrays.stream(m.getParameters()).anyMatch(p -> p.isAnnotationPresent( + Optional.class)); + } + + public static Boolean[] findParameterOptionality(Method m) { + return Arrays.stream(m.getParameters()).map(p -> p.isAnnotationPresent( + Optional.class)).toArray(Boolean[]::new); + } + + public static List fMethodsWithOptional(Class opClass) { + Method superFMethod = SimplificationUtils.findFMethod(opClass); + return Arrays.stream(opClass.getMethods()) // + .filter(m -> m.getName().equals(superFMethod.getName())) // + .filter(m -> m.getParameterCount() == superFMethod.getParameterCount()) // + .filter(m -> hasOptionalAnnotations(m)) // + .collect(Collectors.toList()); + } + + public static Boolean[] generateAllRequiredArray(int num) { + Boolean[] arr = new Boolean[num]; + Arrays.fill(arr, false); + return arr; + } + +} diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationMetadata.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationMetadata.java index 17a82a3d7..bf4d845d8 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationMetadata.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationMetadata.java @@ -226,23 +226,6 @@ public Object[] constructorArgs(Object op) { return args.toArray(); } - /** - * Returns the index of the argument that is both the input and the output. If there is no such argument (i.e. the Op produces a pure output), -1 is returned - * - * @return the index of the mutable argument. - */ - public int ioArgIndex() { - List> inputs = OpUtils.inputs(info.struct()); - Optional> ioArg = inputs.stream().filter(m -> m.isInput() && m.isOutput()).findFirst(); - if(ioArg.isEmpty()) return -1; - Member ioMember = ioArg.get(); - return inputs.indexOf(ioMember); - } - - public boolean pureOutput() { - return ioArgIndex() == -1; - } - public OpInfo info() { return info; } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationUtils.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationUtils.java index fd0f28201..fdb778f2b 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationUtils.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplificationUtils.java @@ -20,6 +20,7 @@ import org.scijava.ops.OpEnvironment; import org.scijava.ops.OpInfo; +import org.scijava.ops.OpUtils; import org.scijava.ops.function.Computers; import org.scijava.ops.matcher.MatchingUtils; import org.scijava.ops.matcher.OpRef; @@ -479,7 +480,7 @@ private static String createFunctionalMethod(SimplificationMetadata metadata) { Method m = ParameterStructs.singularAbstractMethod(metadata.opType()); // determine the name of the output: String opOutput = ""; - int ioIndex = metadata.ioArgIndex(); + int ioIndex = OpUtils.ioArgIndex(metadata.info()); if(ioIndex == -1) { opOutput = "originalOut"; } @@ -497,7 +498,7 @@ private static String createFunctionalMethod(SimplificationMetadata metadata) { sb.append(fMethodPreprocessing(metadata)); // processing - if (metadata.pureOutput()) { + if (OpUtils.hasPureOutput(metadata.info())) { sb.append(metadata.originalOutput().getTypeName() + " " + opOutput + " = "); } sb.append("op." + m.getName() + "("); @@ -511,7 +512,7 @@ private static String createFunctionalMethod(SimplificationMetadata metadata) { sb.append(fMethodPostprocessing(metadata, opOutput)); // if pure output, return it - if (metadata.pureOutput()) { + if (OpUtils.hasPureOutput(metadata.info())) { sb.append("return out;"); } sb.append("}"); @@ -556,7 +557,7 @@ private static String fMethodPostprocessing(SimplificationMetadata metadata, Str // call copy op iff it exists if(metadata.hasCopyOp()) { - int ioIndex = metadata.ioArgIndex(); + int ioIndex = OpUtils.ioArgIndex(metadata.info()); Type ioType = metadata.originalInputs()[ioIndex]; String originalIOArg = "in" + ioIndex; sb.append("copyOp.compute((" + focused.getTypeName() + ") out, (" + ioType.getTypeName() + ") " + originalIOArg + ");"); diff --git a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplifiedOpInfo.java b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplifiedOpInfo.java index 406c53be0..7cd8c14ab 100644 --- a/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplifiedOpInfo.java +++ b/scijava/scijava-ops/src/main/java/org/scijava/ops/simplify/SimplifiedOpInfo.java @@ -13,6 +13,7 @@ import org.scijava.ops.conversionLoss.LossReporter; import org.scijava.ops.core.Op; import org.scijava.ops.matcher.OpMatchingException; +import org.scijava.param.Optional; import org.scijava.param.ParameterStructs; import org.scijava.param.ValidityException; import org.scijava.struct.Member; @@ -63,6 +64,11 @@ public Struct struct() { return struct; } + @Override + public String names() { + return srcInfo.names(); + } + @Override public double priority() { return priority; @@ -224,6 +230,15 @@ private int compareToSimplifiedInfo(SimplifiedOpInfo that) { return theseMembers.hashCode() - thoseMembers.hashCode(); } - + /** + * NB Since the simplified Op is autogenerated, we can rest assured that there + * are no {@link Optional} parameters + */ + @Override + public boolean isOptional(Member m) { + if (!struct.members().contains(m)) throw new IllegalArgumentException( + "Member " + m + " is not a Memeber of OpInfo " + this); + return false; + } } diff --git a/scijava/scijava-ops/src/main/java/org/scijava/param/Optional.java b/scijava/scijava-ops/src/main/java/org/scijava/param/Optional.java new file mode 100644 index 000000000..4c55191fa --- /dev/null +++ b/scijava/scijava-ops/src/main/java/org/scijava/param/Optional.java @@ -0,0 +1,47 @@ + +package org.scijava.param; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a parameter as optional: Ops with optional parameters + * should be callable with or without their Optional arguments + *

    + * This annotation should only be specified on one of the signatures of + * the method (only on the {@link FunctionalInterface}'s method, or only on the + * Op implementation, etc) for purposes of simplicity and readability. Writing + * + *

    + * public interface BiFunctionWithOptional<I1, I2, I3, O> extends
    + * 	Functions.Arity3<>
    + * {
    + * 
    + * 	public O apply(I1 in1, I2 in2, @Optional I3 in3);
    + * }
    + * 
    + * + * and then writing an implementation + * + *
    + * public class Impl implements BiFunctionWithOptional<Double, Double, Double, Double> {
    + * 	public Double apply(Double in1, @Optional Double in2, Double in3) {
    + * 	...
    + * 	}
    + * }
    + * 
    + * + * is confusing and hard to read. Which parameters are optional in this case? Is + * it obvious that {@code in3} is optional just by looking at {@code Impl}? For + * this reason, it should be enforced that the annotation is only on one of the + * method signatures. + * + * @author Gabriel Selzer + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Optional { + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/OpEnvironmentTest.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/OpEnvironmentTest.java index 919d6e5cf..0d05a45a3 100644 --- a/scijava/scijava-ops/src/test/java/org/scijava/ops/OpEnvironmentTest.java +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/OpEnvironmentTest.java @@ -7,9 +7,7 @@ import org.junit.Test; import org.scijava.Priority; import org.scijava.ops.function.Producer; -import org.scijava.ops.matcher.OpClassInfo; import org.scijava.param.Parameter; -import org.scijava.struct.ItemIO; import org.scijava.types.GenericTyped; import org.scijava.types.Nil; @@ -42,7 +40,7 @@ public void testBakeType() { @Test public void testClassOpification() { - OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class); + OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class, "foo"); Assert.assertEquals(OpifyOp.class.getName(), opifyOpInfo.implementationName()); // assert default priority Assert.assertEquals(Priority.NORMAL, opifyOpInfo.priority(), 0.); @@ -50,7 +48,7 @@ public void testClassOpification() { @Test public void testClassOpificationWithPriority() { - OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class, Priority.HIGH); + OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class, "foo", Priority.HIGH); Assert.assertEquals(OpifyOp.class.getName(), opifyOpInfo.implementationName()); // assert default priority Assert.assertEquals(Priority.HIGH, opifyOpInfo.priority(), 0.); @@ -59,8 +57,8 @@ public void testClassOpificationWithPriority() { @Test public void testRegister() { String opName = "test.opifyOp"; - OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class, Priority.HIGH); - ops.env().register(opifyOpInfo, opName); + OpInfo opifyOpInfo = ops.env().opify(OpifyOp.class, opName, Priority.HIGH); + ops.env().register(opifyOpInfo); String actual = ops.op(opName).input().outType(String.class).create(); diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptional.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptional.java new file mode 100644 index 000000000..926fa3807 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptional.java @@ -0,0 +1,14 @@ + +package org.scijava.ops.reduce; + +import org.scijava.ops.function.Functions; +import org.scijava.param.Optional; + +@FunctionalInterface +public interface BiFunctionWithOptional extends + Functions.Arity3 +{ + + @Override + public O apply(I1 in1, I2 in2, @Optional I3 in3); +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptionalReducer.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptionalReducer.java new file mode 100644 index 000000000..a00271044 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/BiFunctionWithOptionalReducer.java @@ -0,0 +1,36 @@ +package org.scijava.ops.reduce; + +import java.lang.reflect.Type; + +import org.scijava.ops.OpInfo; +import org.scijava.ops.OpUtils; +import org.scijava.ops.function.Functions; +import org.scijava.param.ParameterStructs; +import org.scijava.plugin.Plugin; +import org.scijava.types.Types; + +@Plugin(type = InfoReducer.class) +public class BiFunctionWithOptionalReducer implements InfoReducer { + + @Override + public boolean canReduce(OpInfo info) { + return ParameterStructs.findFunctionalInterface(Types.raw(info.opType())) == BiFunctionWithOptional.class; + } + + @Override + public ReducedOpInfo reduce(OpInfo info, int numReductions) { + if (numReductions != 1) throw new UnsupportedOperationException(); + int reducedArity = 3 - numReductions; + Class reducedRawType = Functions.ALL_FUNCTIONS.get(reducedArity); + Type[] inputTypes = OpUtils.inputTypes(info.struct()); + Type outputType = OpUtils.outputType(info.struct()); + Type[] newTypes = new Type[reducedArity + 1]; + for(int i = 0; i < reducedArity; i++) { + newTypes[i] = inputTypes[i]; + } + newTypes[newTypes.length - 1] = outputType; + Type reducedOpType = Types.parameterize(reducedRawType, newTypes); + return new ReducedOpInfo(info, reducedOpType, numReductions); + } + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsFromIFaceTest.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsFromIFaceTest.java new file mode 100644 index 000000000..05d305554 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsFromIFaceTest.java @@ -0,0 +1,79 @@ +package org.scijava.ops.reduce; + +import org.junit.Assert; +import org.junit.Test; +import org.scijava.ops.AbstractTestEnvironment; +import org.scijava.ops.OpField; +import org.scijava.ops.OpMethod; +import org.scijava.ops.core.OpCollection; +import org.scijava.plugin.Plugin; + +@Plugin(type = OpCollection.class) +public class OptionalArgumentsFromIFaceTest extends AbstractTestEnvironment{ + + @OpMethod(names = "test.optionalSubtract", type = BiFunctionWithOptional.class) + public static Double foo(Double i1, Double i2, Double i3) { + if (i3 == null) i3 = 0.; + return i1 - i2 - i3; + } + + @Test + public void testMethodWithOneOptional() { + Double o = ops.env().op("test.optionalSubtract").input(2., 5., 7.).outType(Double.class).apply(); + Double expected = -10.0; + Assert.assertEquals(expected, o); + } + + @Test + public void testMethodWithoutOptionals() { + Double o = ops.env().op("test.optionalSubtract").input(2., 5.).outType(Double.class).apply(); + Double expected = -3.0; + Assert.assertEquals(expected, o); + } + + @OpField(names = "test.optionalAnd", params = "in1, in2, in3") + public final BiFunctionWithOptional bar = + (in1, in2, in3) -> { + if (in3 == null) in3 = true; + return in1 & in2 & in3; + }; + + @Test + public void testFieldWithOptionals() { + Boolean in1 = true; + Boolean in2 = true; + Boolean in3 = false; + Boolean o = ops.env().op("test.optionalAnd").input(in1, in2, in3).outType(Boolean.class).apply(); + Boolean expected = false; + Assert.assertEquals(expected, o); + } + + @Test + public void testFieldWithoutOptionals() { + Boolean in1 = true; + Boolean in2 = true; + Boolean o = ops.env().op("test.optionalAnd").input(in1, in2).outType(Boolean.class).apply(); + Boolean expected = true; + Assert.assertEquals(expected, o); + } + + @Test + public void testClassWithOptionals() { + Boolean in1 = true; + Boolean in2 = false; + Boolean in3 = false; + Boolean o = ops.env().op("test.optionalOr").input(in1, in2, in3).outType(Boolean.class).apply(); + Boolean expected = true; + Assert.assertEquals(expected, o); + } + + @Test + public void testClassWithoutOptionals() { + Boolean in1 = true; + Boolean in2 = false; + Boolean o = ops.env().op("test.optionalOr").input(in1, in2).outType(Boolean.class).apply(); + Boolean expected = true; + Assert.assertEquals(expected, o); + } + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsTest.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsTest.java new file mode 100644 index 000000000..b36f99568 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/OptionalArgumentsTest.java @@ -0,0 +1,121 @@ +package org.scijava.ops.reduce; + +import java.util.Arrays; + +import org.junit.Assert; +import org.junit.Test; +import org.scijava.ops.AbstractTestEnvironment; +import org.scijava.ops.OpField; +import org.scijava.ops.OpMethod; +import org.scijava.ops.core.OpCollection; +import org.scijava.ops.function.Computers; +import org.scijava.ops.function.Functions; +import org.scijava.param.Optional; +import org.scijava.plugin.Plugin; + +@Plugin(type = OpCollection.class) +public class OptionalArgumentsTest extends AbstractTestEnvironment{ + + @Test + public void testClassWithTwoOptionals() { + Double sum = ops.env().op("test.optionalAdd").input(2.0, 5.0, 7.0).outType(Double.class).apply(); + Double expected = 14.0; + Assert.assertEquals(expected, sum); + } + + @Test + public void testClassWithOneOptional() { + Double sum = ops.env().op("test.optionalAdd").input(2.0, 5.0).outType(Double.class).apply(); + Double expected = 7.0; + Assert.assertEquals(expected, sum); + } + + @Test + public void testClassWithoutOptionals() { + Double sum = ops.env().op("test.optionalAdd").input(2.0).outType(Double.class).apply(); + Double expected = 2.0; + Assert.assertEquals(expected, sum); + } + + @OpField(names = "test.optionalMultiply") + public final Computers.Arity3 optionalField = + new Computers.Arity3<>() + { + + @Override + public void compute(Double[] in1, @Optional Double[] in2, + @Optional Double[] in3, Double[] out) + { + if (in2 == null) { + in2 = new Double[in1.length]; + Arrays.fill(in2, 1.); + } + if (in3 == null) { + in3 = new Double[in1.length]; + Arrays.fill(in3, 1.); + } + for (int i = 0; i < in1.length; i++) { + out[i] = in1[i] * in2[i] * in3[i]; + } + } + }; + + @Test + public void testFieldWithTwoOptionals() { + Double[] d1 = {2.0}; + Double[] d2 = {5.0}; + Double[] d3 = {7.0}; + Double[] o = {50.0}; + ops.env().op("test.optionalMultiply").input(d1, d2, d3).output(o).compute(); + Double expected = 70.0; + Assert.assertEquals(expected, o[0]); + } + + @Test + public void testFieldWithOneOptional() { + Double[] d1 = {2.0}; + Double[] d2 = {5.0}; + Double[] o = {50.0}; + ops.env().op("test.optionalMultiply").input(d1, d2).output(o).compute(); + Double expected = 10.0; + Assert.assertEquals(expected, o[0]); + } + + @Test + public void testFieldWithoutOptionals() { + Double[] d1 = {2.0}; + Double[] o = {50.0}; + ops.env().op("test.optionalMultiply").input(d1).output(o).compute(); + Double expected = 2.0; + Assert.assertEquals(expected, o[0]); + } + + @OpMethod(names = "test.optionalConcatenate", type = Functions.Arity3.class) + public static String optionalMethod(String in1, @Optional String in2, @Optional String in3) { + if (in2 == null) in2 = ""; + if (in3 == null) in3 = ""; + return in1.concat(in2).concat(in3); + } + + @Test + public void testMethodWithTwoOptionals() { + String out = ops.env().op("test.optionalConcatenate").input("a", "b", "c").outType(String.class).apply(); + String expected = "abc"; + Assert.assertEquals(expected, out); + } + + @Test + public void testMethodWithOneOptional() { + String out = ops.env().op("test.optionalConcatenate").input("a", "b").outType(String.class).apply(); + String expected = "ab"; + Assert.assertEquals(expected, out); + } + + @Test + public void testMethodWithoutOptionals() { + String out = ops.env().op("test.optionalConcatenate").input("a").outType(String.class).apply(); + String expected = "a"; + Assert.assertEquals(expected, out); + } + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/ReductionWithDependenciesTest.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/ReductionWithDependenciesTest.java new file mode 100644 index 000000000..2cb99ac4b --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/ReductionWithDependenciesTest.java @@ -0,0 +1,65 @@ +package org.scijava.ops.reduce; + +import java.util.function.Function; + +import org.junit.Test; +import org.junit.Assert; +import org.scijava.ops.AbstractTestEnvironment; +import org.scijava.ops.OpDependency; +import org.scijava.ops.OpMethod; +import org.scijava.ops.core.OpCollection; +import org.scijava.ops.function.Producer; +import org.scijava.param.Optional; +import org.scijava.plugin.Plugin; + +@Plugin(type = OpCollection.class) +public class ReductionWithDependenciesTest extends AbstractTestEnvironment{ + + @OpMethod(names = "test.fooDependency", type = Producer.class) + public static Double bar() { + return 5.; + } + + @OpMethod(names = "test.optionalWithDependency", type = Function.class) + public static Double foo(@OpDependency(name = "test.fooDependency") Producer bar, @Optional Double opt) { + if (opt == null) opt = 0.; + return bar.create() + opt; + } + + @OpMethod(names = "test.optionalWithDependency2", type = Function.class) + public static Double foo(@Optional Double opt, @OpDependency(name = "test.fooDependency") Producer bar) { + if (opt == null) opt = 0.; + return bar.create() + opt; + } + + @Test + public void testDependencyFirstMethodWithOptional() { + Double opt = 7.; + Double o = ops.op("test.optionalWithDependency").input(opt).outType(Double.class).apply(); + Double expected = 12.; + Assert.assertEquals(expected, o); + } + + @Test + public void testDependencyFirstMethodWithoutOptional() { + Double o = ops.op("test.optionalWithDependency").input().outType(Double.class).create(); + Double expected = 5.; + Assert.assertEquals(expected, o); + } + + @Test + public void testDependencySecondMethodWithOptional() { + Double opt = 7.; + Double o = ops.op("test.optionalWithDependency2").input(opt).outType(Double.class).apply(); + Double expected = 12.; + Assert.assertEquals(expected, o); + } + + @Test + public void testDependencySecondMethodWithoutOptional() { + Double o = ops.op("test.optionalWithDependency2").input().outType(Double.class).create(); + Double expected = 5.; + Assert.assertEquals(expected, o); + } + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalArg.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalArg.java new file mode 100644 index 000000000..cbeccc578 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalArg.java @@ -0,0 +1,27 @@ + +package org.scijava.ops.reduce; + +import org.scijava.ops.core.Op; +import org.scijava.param.Optional; +import org.scijava.param.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.struct.ItemIO; +import org.scijava.ops.function.Functions; + +@Plugin(type = Op.class, name = "test.optionalAdd") +@Parameter(key = "in1") +@Parameter(key = "in2") +@Parameter(key = "in3") +@Parameter(key = "out", itemIO = ItemIO.OUTPUT) +public class TestOpOptionalArg implements + Functions.Arity3 +{ + + @Override + public Double apply(Double t, @Optional Double u, @Optional Double v) { + if (u == null) u = 0.; + if (v == null) v = 0.; + return t + u + v; + } + +} diff --git a/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalFromIFace.java b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalFromIFace.java new file mode 100644 index 000000000..be439a081 --- /dev/null +++ b/scijava/scijava-ops/src/test/java/org/scijava/ops/reduce/TestOpOptionalFromIFace.java @@ -0,0 +1,21 @@ +package org.scijava.ops.reduce; + +import org.scijava.struct.ItemIO; +import org.scijava.ops.core.Op; +import org.scijava.param.Parameter; +import org.scijava.plugin.Plugin; + +@Plugin(type = Op.class, name = "test.optionalOr") +@Parameter(key = "in1") +@Parameter(key = "in2") +@Parameter(key = "in3") +@Parameter(key = "out", itemIO = ItemIO.OUTPUT) +public class TestOpOptionalFromIFace implements BiFunctionWithOptional{ + + @Override + public Boolean apply(Boolean in1, Boolean in2, Boolean in3) { + if (in3 == null) in3 = false; + return in1 | in2 | in3; + } + +}