6.7 KiB
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.
import lyng.operators
Why This Exists
If your class defines:
class Amount(val value: Int) {
fun plus(other: Amount) = Amount(value + other.value)
}
then:
Amount(1) + Amount(2)
works, because the left operand already knows how to add another Amount.
But:
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:
- converts
L -> C - converts
R -> C - evaluates the operator as
C op C
So the registry is a bridge, not a separate arithmetic engine.
API
OperatorInterop.register(
leftClass,
rightClass,
commonClass,
operators,
leftToCommon,
rightToCommon
)
Parameters:
leftClass: original left operand classrightClass: original right operand classcommonClass: class that will actually execute the operator methodsoperators: list of operators enabled for this pairleftToCommon: conversion from left operand to common classrightToCommon: conversion from right operand to common class
Supported Operators
BinaryOperator values:
PlusMinusMulDivModCompareEquals
Meaning:
Compareenables<,<=,>,>=, and<=>Equalsenables==and!=
Minimal Working Example
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:
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:
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 = IntorRealrightClass = DecimalcommonClass = Decimal- convert built-ins into
Decimal - leave
Decimalvalues 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:
class Rational(...)
2. Implement operators on that class
The common class should define the operations you plan to register.
Example:
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
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
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:
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:
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
compareToif you want ordering operators - register
Equalswhenever mixed equality should work - keep the registered operator list minimal and accurate
For decimal-like semantics, also read Decimal.md.