89 lines
4.3 KiB
Markdown
89 lines
4.3 KiB
Markdown
|
# Inheritance
|
||
|
|
||
|
## Status
|
||
|
ACCEPTED
|
||
|
|
||
|
Discussed by Alex Black and Paul Dubs on 29. November 2019 and 2. December 2019.
|
||
|
|
||
|
## Context
|
||
|
In many cases ops have a similar interface. For example all transform ops take a single input, but some of them take
|
||
|
additional arguments; all pairwise ops take two inputs, and so on. The documentation of those ops is often the result
|
||
|
of copy & paste with just a few little modifications, and changing anything later on suddenly becomes a huge undertaking
|
||
|
because what should effectively be a change in a single place, has to be changed in many places.
|
||
|
|
||
|
Another issue that copy & paste based definitions bring to the table is that this practice effectively makes any
|
||
|
relationship between those ops implicit.
|
||
|
|
||
|
When defining ops with the DSL we prefer to make things as explicit as possible, while also reducing repetition and
|
||
|
boilerplate.
|
||
|
|
||
|
The existing inheritance mechanism, added without a formal proposal at the beginning of the project, allows the
|
||
|
definition of an abstract base op. The new op based on it, can copy any of the given parts before it gets to define its
|
||
|
own properties. This approach has the problem that we can't inherit from multiple base ops, and that we do not get any
|
||
|
direct access to its fields for ease of use.
|
||
|
|
||
|
# Decision
|
||
|
We introduce an explicit mixin mechanism `Mixin("name") {...}` which can define any parts of any op, but isn't an Op
|
||
|
definition on its own. Mixins can be defined at top-level, thereby being usable across namespaces.
|
||
|
|
||
|
A mixin is mixed into an op with `useMixin(mixinReference, ...options...)` within an op context. It will add all (if
|
||
|
not otherwise configured) definitions of the mixin to the current op as if they were copied into its place.
|
||
|
If `useMixin(ref)` is used as the first thing within an op definition, then it will behave exactly like the old
|
||
|
inheritance mechanism.
|
||
|
|
||
|
`useMixin(ref)` returns a holder object, that can be used to reference its parameters.
|
||
|
|
||
|
The available options on `useMixin` are `keepInputs`, `keepArgs`, `keepOutputs`, `keepSignatures`, `keepDoc`,
|
||
|
`keepConstraints`. They default to `true`.
|
||
|
|
||
|
If there is a naming conflict between mixins or between mixin and op definition, the last definition wins.
|
||
|
|
||
|
### Example
|
||
|
```kotlin
|
||
|
|
||
|
val indexAccum = Mixin("indexAccum"){
|
||
|
legacy = true
|
||
|
javaPackage = "org.nd4j.linalg.api.ops.impl.indexaccum"
|
||
|
val input = Input(NUMERIC, "in") { description = "Input variable" }
|
||
|
val keepDims = Arg(BOOL, "keepDims") { description = "If true: keep the dimensions that are reduced on (as length 1). False: remove the reduction dimensions"; defaultValue = false }
|
||
|
val dims = Arg(INT, "dimensions"){ count = AtLeast(1); description = "Dimensions to reduce over. If dimensions are not specified, full array reduction is performed" }
|
||
|
Output(NUMERIC, "output"){ description = "Reduced array of rank (input rank - num dimensions)" }
|
||
|
|
||
|
Signature(input, dims)
|
||
|
AllParamSignature(withOutput = false)
|
||
|
}
|
||
|
|
||
|
|
||
|
Namespace("math"){
|
||
|
Op("firstIndex") {
|
||
|
val idxAccum = useMixin(indexAccum, keepSignatures=false)
|
||
|
var c = Arg(CONDITION, "condition") { description = "Condition to check on input variable" }
|
||
|
Signature(idxAccum.input("in"), c, idxAccum.arg("dimensions"))
|
||
|
Signature(idxAccum.input("in"), c, idxAccum.arg("keepDims"), idxAccum.arg("dimensions"))
|
||
|
|
||
|
Doc(Language.ANY, DocScope.ALL){
|
||
|
"""
|
||
|
First index reduction operation.
|
||
|
Returns a variable that contains the index of the first element that matches the specified condition (for each
|
||
|
slice along the specified dimensions)
|
||
|
Note that if keepDims = true, the output variable has the same rank as the input variable,
|
||
|
with the reduced dimensions having size 1. This can be useful for later broadcast operations (such as subtracting
|
||
|
the mean along a dimension).
|
||
|
Example: if input has shape [a,b,c] and dimensions=[1] then output has shape:
|
||
|
keepDims = true: [a,1,c]
|
||
|
keepDims = false: [a,c]
|
||
|
""".trimIndent()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
|
||
|
## Consequences
|
||
|
### Advantages
|
||
|
* We can have multiple inheritance
|
||
|
* We can share op similarities across namespaces
|
||
|
* We get explicit access to parameters defined in mixins
|
||
|
|
||
|
### Disadvantages
|
||
|
* We have to adapt our current usage
|