Skip to content
Noodle
InstallLearnPlayground
GitHub

Operator overloading

Noodle overloads existing operators through standard Prelude interfaces. A type supplies an extension provider for the corresponding interface; the language does not allow declarations of new operator symbols or changes to precedence and associativity.

Primitive Int and Double operations use built-in semantics. Providers are used when an overloadable operator has a non-primitive receiver.

This Score datatype associates an IAdd[Score] provider. The + expression then resolves to that provider’s add method:

datatype Score
extensions ScoreAdd
Score(Int)
end
extension ScoreAdd : IAdd[Score] for Score
type Output = Score
method add(left : Score, right : Score) -> Score do
Score::Score(left._0 + right._0)
end
end
func combine(left : Score, right : Score) -> Score do
left + right
end

The datatype’s extensions clause makes ScoreAdd an associated provider, so callers do not need to activate or pass it explicitly. Providers can also come from locally enabled extensions or contextual extension questions; the usual interface and extension resolution rules still apply.

An overloadable operator uses the same provider and method lookup as the corresponding method form:

Operator Prelude interface Method form
-x INegate x.negate()
x + y IAdd[Rhs] x.add(y)
x - y ISubtract[Rhs] x.subtract(y)
x * y IMultiply[Rhs] x.multiply(y)
x / y IDivide[Rhs] x.divide(y)
x % y IRemainder[Rhs] x.remainder(y)
x ++ y IConcat[Rhs] x.concat(y)
x == y IEqual x.equal(y)
x != y IEqual !(x.equal(y))
x < y ICompare x.compare(y) < 0
x <= y ICompare x.compare(y) <= 0
x > y ICompare x.compare(y) > 0
x >= y ICompare x.compare(y) >= 0
x <=> y ICompare Ordering view of Int applied to x.compare(y)

IEqual and ICompare are homogeneous: the right operand has the receiver’s type. equal returns Bool, while compare returns a negative integer, zero, or a positive integer. The three-way operator <=> classifies that integer sign into the standard Ordering datatype (Less, Equal, or Greater), which can be pattern matched directly. Many structural values can use compiler-generated providers; see Structural capabilities.

++ on a String receiver keeps the built-in concatenation; other receivers resolve IConcat[Rhs]. The standard library associates IConcat[String] for String with that built-in concatenation, so generic code can constrain concatenation uniformly.

switch left <=> right
case Ordering::Less then "before"
case Ordering::Equal then "same"
case Ordering::Greater then "after"
end

Ordering::Less, Ordering::Equal, and Ordering::Greater are also view patterns over Int, so an integer sign can be classified directly:

switch delta
case Ordering::Less then "decreased"
case Ordering::Equal then "unchanged"
case Ordering::Greater then "increased"
end

Checked indexing, source[index]!, follows the related built-in IIndexGet[Index] for Source protocol. It selects an associated Element type and propagates IndexError, but it is not an arithmetic operator interface.

Arithmetic interfaces take an explicit Rhs type and define an associated Output type. Neither has to equal the receiver type:

datatype Vec2
extensions Vec2Scale
Vec2{x: Double, y: Double}
end
extension Vec2Scale : IMultiply[Double] for Vec2
type Output = Vec2
method multiply(vector : Vec2, factor : Double) -> Vec2 do
Vec2::Vec2{x: vector.x * factor, y: vector.y * factor}
end
end
func scale(vector : Vec2, factor : Double) -> Vec2 do
vector * factor
end

This provider defines Vec2 * Double -> Vec2. It does not define Double * Vec2, Vec2 * Vec2, or any other operator pairing. Each pairing needs its own applicable provider.

State operator requirements in generic code

Section titled “State operator requirements in generic code”

Generic code requests the exact capability it uses. Constraining Output keeps the function’s result type equal to T:

func add_same[T](
left : T,
right : T,
?{extension Add: IAdd[T, Output = T] for T},
) -> T do
left + right
end

Operator resolution matches the receiver against Self and the right operand against Rhs. When Rhs is not otherwise known, resolution first tries the homogeneous pairing Rhs = Self. No applicable provider produces an undefined-operator diagnostic; multiple applicable providers produce an ambiguous-operator diagnostic. An expected result type does not choose among otherwise ambiguous providers.

The associated methods remain available as ordinary dot calls, such as left.add(right) and left.equal(right). Use a named contextual provider such as Add.add(left, right) when the provider choice itself should be explicit.

The following forms keep fixed language semantics:

  • logical !, &&, and ||;
  • unary +;
  • integer bitwise ~~~, &&&, ^^^, |||, <<<, >>, and >>>;
  • string concatenation ++;
  • option fallback ??.

An overloaded receiver is evaluated exactly once. Ordinary binary operands are evaluated left to right before the selected method runs, matching the corresponding method call’s evaluation behavior.

Next: Open types.