310 lines
6.7 KiB
Markdown
310 lines
6.7 KiB
Markdown
# Operator Interop Registry
|
|
|
|
`lyng.operators` provides a runtime registry for mixed-class binary operators.
|
|
|
|
Import it when you want expressions such as:
|
|
|
|
- `1 + MyType(...)`
|
|
- `2 < MyType(...)`
|
|
- `3 == MyType(...)`
|
|
|
|
to work without modifying the built-in `Int` or `Real` classes.
|
|
|
|
```lyng
|
|
import lyng.operators
|
|
```
|
|
|
|
## Why This Exists
|
|
|
|
If your class defines:
|
|
|
|
```lyng
|
|
class Amount(val value: Int) {
|
|
fun plus(other: Amount) = Amount(value + other.value)
|
|
}
|
|
```
|
|
|
|
then:
|
|
|
|
```lyng
|
|
Amount(1) + Amount(2)
|
|
```
|
|
|
|
works, because the left operand already knows how to add another `Amount`.
|
|
|
|
But:
|
|
|
|
```lyng
|
|
1 + Amount(2)
|
|
```
|
|
|
|
does not naturally work, because `Int` has not been rewritten to know about `Amount`.
|
|
|
|
The operator interop registry solves exactly that problem.
|
|
|
|
## Mental Model
|
|
|
|
Registration describes a mixed pair:
|
|
|
|
- left class `L`
|
|
- right class `R`
|
|
- common class `C`
|
|
|
|
When Lyng sees `L op R`, it:
|
|
|
|
1. converts `L -> C`
|
|
2. converts `R -> C`
|
|
3. evaluates the operator as `C op C`
|
|
|
|
So the registry is a bridge, not a separate arithmetic engine.
|
|
|
|
## API
|
|
|
|
```lyng
|
|
OperatorInterop.register(
|
|
leftClass,
|
|
rightClass,
|
|
commonClass,
|
|
operators,
|
|
leftToCommon,
|
|
rightToCommon
|
|
)
|
|
```
|
|
|
|
Parameters:
|
|
|
|
- `leftClass`: original left operand class
|
|
- `rightClass`: original right operand class
|
|
- `commonClass`: class that will actually execute the operator methods
|
|
- `operators`: list of operators enabled for this pair
|
|
- `leftToCommon`: conversion from left operand to common class
|
|
- `rightToCommon`: conversion from right operand to common class
|
|
|
|
## Supported Operators
|
|
|
|
`BinaryOperator` values:
|
|
|
|
- `Plus`
|
|
- `Minus`
|
|
- `Mul`
|
|
- `Div`
|
|
- `Mod`
|
|
- `Compare`
|
|
- `Equals`
|
|
|
|
Meaning:
|
|
|
|
- `Compare` enables `<`, `<=`, `>`, `>=`, and `<=>`
|
|
- `Equals` enables `==` and `!=`
|
|
|
|
## Minimal Working Example
|
|
|
|
```lyng
|
|
package test.decimalbox
|
|
import lyng.operators
|
|
|
|
class DecimalBox(val value: Int) {
|
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
|
fun minus(other: DecimalBox) = DecimalBox(value - other.value)
|
|
fun mul(other: DecimalBox) = DecimalBox(value * other.value)
|
|
fun div(other: DecimalBox) = DecimalBox(value / other.value)
|
|
fun mod(other: DecimalBox) = DecimalBox(value % other.value)
|
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
|
}
|
|
|
|
OperatorInterop.register(
|
|
Int,
|
|
DecimalBox,
|
|
DecimalBox,
|
|
[
|
|
BinaryOperator.Plus,
|
|
BinaryOperator.Minus,
|
|
BinaryOperator.Mul,
|
|
BinaryOperator.Div,
|
|
BinaryOperator.Mod,
|
|
BinaryOperator.Compare,
|
|
BinaryOperator.Equals
|
|
],
|
|
{ x: Int -> DecimalBox(x) },
|
|
{ x: DecimalBox -> x }
|
|
)
|
|
```
|
|
|
|
Then:
|
|
|
|
```lyng
|
|
import test.decimalbox
|
|
|
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
|
assertEquals(DecimalBox(1), 3 - DecimalBox(2))
|
|
assertEquals(DecimalBox(8), 4 * DecimalBox(2))
|
|
assertEquals(DecimalBox(4), 8 / DecimalBox(2))
|
|
assertEquals(DecimalBox(1), 7 % DecimalBox(2))
|
|
assert(1 < DecimalBox(2))
|
|
assert(2 <= DecimalBox(2))
|
|
assert(3 > DecimalBox(2))
|
|
assert(2 == DecimalBox(2))
|
|
assert(2 != DecimalBox(3))
|
|
```
|
|
|
|
## How Decimal Uses It
|
|
|
|
`lyng.decimal` uses this same mechanism so that:
|
|
|
|
```lyng
|
|
import lyng.decimal
|
|
|
|
1 + 2.d
|
|
0.5 + 1.d
|
|
2 == 2.d
|
|
3 > 2.d
|
|
```
|
|
|
|
work naturally even though `Int` and `Real` themselves were not edited to know `Decimal`.
|
|
|
|
The shape is:
|
|
|
|
- `leftClass = Int` or `Real`
|
|
- `rightClass = Decimal`
|
|
- `commonClass = Decimal`
|
|
- convert built-ins into `Decimal`
|
|
- leave `Decimal` values unchanged
|
|
|
|
## Step-By-Step Pattern For Your Own Type
|
|
|
|
### 1. Pick the common class
|
|
|
|
Choose one class that will be the actual arithmetic domain.
|
|
|
|
For numeric-like types, that is usually your own class:
|
|
|
|
```lyng
|
|
class Rational(...)
|
|
```
|
|
|
|
### 2. Implement operators on that class
|
|
|
|
The common class should define the operations you plan to register.
|
|
|
|
Example:
|
|
|
|
```lyng
|
|
class Rational(val num: Int, val den: Int) {
|
|
fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
|
fun minus(other: Rational) = Rational(num * other.den - other.num * den, den * other.den)
|
|
fun mul(other: Rational) = Rational(num * other.num, den * other.den)
|
|
fun div(other: Rational) = Rational(num * other.den, den * other.num)
|
|
fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
|
|
|
static fun fromInt(value: Int) = Rational(value, 1)
|
|
}
|
|
```
|
|
|
|
### 3. Register the mixed pair
|
|
|
|
```lyng
|
|
import lyng.operators
|
|
|
|
OperatorInterop.register(
|
|
Int,
|
|
Rational,
|
|
Rational,
|
|
[
|
|
BinaryOperator.Plus,
|
|
BinaryOperator.Minus,
|
|
BinaryOperator.Mul,
|
|
BinaryOperator.Div,
|
|
BinaryOperator.Compare,
|
|
BinaryOperator.Equals
|
|
],
|
|
{ x: Int -> Rational.fromInt(x) },
|
|
{ x: Rational -> x }
|
|
)
|
|
```
|
|
|
|
### 4. Use it
|
|
|
|
```lyng
|
|
assertEquals(Rational(3, 2), 1 + Rational(1, 2))
|
|
assert(Rational(3, 2) == Rational(3, 2))
|
|
assert(2 > Rational(3, 2))
|
|
```
|
|
|
|
## Registering More Than One Built-in Type
|
|
|
|
If you want both `Int + MyType` and `Real + MyType`, register both pairs explicitly:
|
|
|
|
```lyng
|
|
OperatorInterop.register(
|
|
Int,
|
|
MyType,
|
|
MyType,
|
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
|
{ x: Int -> MyType.fromInt(x) },
|
|
{ x: MyType -> x }
|
|
)
|
|
|
|
OperatorInterop.register(
|
|
Real,
|
|
MyType,
|
|
MyType,
|
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
|
{ x: Real -> MyType.fromReal(x) },
|
|
{ x: MyType -> x }
|
|
)
|
|
```
|
|
|
|
Each mixed pair is independent.
|
|
|
|
## Pure Lyng Registration
|
|
|
|
This mechanism is intentionally useful from pure Lyng code, not only from Kotlin-backed modules.
|
|
|
|
That means you can:
|
|
|
|
- declare a class in Lyng
|
|
- define its operators in Lyng
|
|
- register mixed operand bridges in Lyng
|
|
|
|
without touching compiler internals.
|
|
|
|
## Where To Register
|
|
|
|
Register once during module initialization.
|
|
|
|
Top-level module code is a good place:
|
|
|
|
```lyng
|
|
package my.rational
|
|
import lyng.operators
|
|
|
|
class Rational(...)
|
|
|
|
OperatorInterop.register(...)
|
|
```
|
|
|
|
That keeps registration close to the type declaration and makes importing the module enough to activate the interop.
|
|
|
|
## What Registration Does Not Do
|
|
|
|
The registry does not:
|
|
|
|
- invent operators your common class does not implement
|
|
- change the original `Int`, `Real`, or other built-ins
|
|
- automatically cover every class pair
|
|
- replace normal method overload resolution when the left-hand class already knows what to do
|
|
|
|
It only teaches Lyng how to bridge a specific mixed pair into a common class for the listed operators.
|
|
|
|
## Recommended Design Rules
|
|
|
|
If you want interop to feel natural:
|
|
|
|
- choose one obvious common class
|
|
- make conversions explicit and unsurprising
|
|
- implement `compareTo` if you want ordering operators
|
|
- register `Equals` whenever mixed equality should work
|
|
- keep the registered operator list minimal and accurate
|
|
|
|
For decimal-like semantics, also read [Decimal.md](Decimal.md).
|