Skip to content
Noodle
InstallLearnPlayground
GitHub

Interfaces and extensions

Interfaces and extensions separate an operation’s contract from its implementation. They are useful for generic algorithms, methods on types you do not own, operator capabilities, and multiple explicit providers.

An interface has an implicit subject type named Self:

interface IMeasure
method magnitude(self : Self) -> Int
end

Members marked method participate in dot-call lookup. An ordinary func member remains a dictionary function and is called through a provider value. The distinction matters most for open types, where only interface method members become dynamically dispatched slots.

interface IMeasure
method magnitude(self : Self) -> Int
end
datatype Distance
extensions DistanceMeasure
Distance(Int)
end
extension DistanceMeasure : IMeasure for Distance
method magnitude(distance : Distance) -> Int do
distance._0
end
end
func main() -> Unit do
distance = Distance::Distance(12);
Debug.trace(distance.magnitude());
end

The extension must satisfy the interface member names and types after Self is replaced with Distance.

The datatype’s extensions DistanceMeasure clause associates that provider with the nominal Distance type. Associated providers are considered when a method or contextual capability is requested for that owner.

Association exposes provider metadata, not an ordinary source name. Keep an associated extension private unless callers also need to name that specific provider explicitly.

interface IMeasure
method magnitude(self : Self) -> Int
end
datatype Distance
extensions DistanceMeasure
Distance(Int)
end
extension DistanceMeasure : IMeasure for Distance
method magnitude(distance : Distance) -> Int do
distance._0
end
end
func measure[T](
value : T,
?{extension Measure: IMeasure for T},
) -> Int do
Measure.magnitude(value)
end
func main() -> Unit do
distance = Distance::Distance(12);
Debug.trace(measure(distance));
end

At measure(distance), the compiler finds the associated DistanceMeasure. Exactly one applicable provider is required. No provider is a missing-extension error; several equally applicable providers are an ambiguous-extension error.

An extension without an interface can add methods directly to a subject type:

extension IntParity for Int
method is_even(value : Int) -> Bool do
value % 2 == 0
end
end
func main() -> Unit do
Debug.trace(42.is_even());
end

Only method declarations participate in dot-call lookup. A direct extension’s ordinary func members are callable through its provider value, not as methods.

For the full func/method comparison, including receiver rules, open-type dispatch, and value-level witnesses, see Open types.

For receiver.name(arguments), a directly callable record field takes priority. Otherwise, the compiler searches visible and associated extension providers for a unique method whose receiver matches. Provider selection does not use the expected result type to choose between ambiguous providers.

An exported extension from another module is not automatically active merely because that module is visible. Use enable extensions Module.Extension near the top of the consuming source module when an unassociated provider must participate in local method and contextual lookup. Activation is local to that source module and is not re-exported.

The activation must name an exported extension in a visible module. It is placed after an optional module header and before declarations. It does not change the provider’s public signature and does not make the extension active in files that import the current module.

The provider and consumer would normally be separate source files in the same package (or in visible packages). This complete two-file example shows the placement and the qualified name:

provider.nl:
module Provider
export extension IntCompare : ICompare for Int
method compare(left : Int, right : Int) -> Int do
if left < right then -1 elsif left > right then 1 else 0 end
end
end
consumer.nl:
module Consumer
enable extensions Provider.IntCompare
func compare_with_zero(value : Int) -> Int do
value.compare(0)
end
func main() -> Unit do
Debug.trace(compare_with_zero(1));
end

The activation must name an exported, unassociated provider. Associated providers, such as an extension listed after a datatype’s extensions clause, do not need an activation at each call site.

When an API has more than one possible provider, or when the choice is part of the API’s meaning, pass the provider explicitly. A contextual question binds the selected dictionary under an uppercase name:

interface IFormat
func format(value : Self) -> String
end
extension DecimalFormat : IFormat for Int
func format(value : Int) -> String do
value.to_string() ++ ".00"
end
end
func render[T](
value : T,
?{extension Format: IFormat for T},
) -> String do
Format.format(value)
end
func main() -> Unit do
Debug.trace(render(12, ?{Format: DecimalFormat}));
end

?{Format: DecimalFormat} is an explicit answer: it bypasses provider resolution for that question and passes the named extension dictionary to the function. The witness is a normal immutable value inside the function, so its ordinary dictionary members are called as Format.format(value). The contextual question’s extension form, candidate resolution, and associated providers are covered in Question parameters.

The standard library uses these same rules for arithmetic, equality, ordering, debugging, iteration, indexing, and conversions.

The standard equality, comparison, hashing, and debugging interfaces also associate providers that the compiler specializes for concrete data shapes. Continue with Structural capabilities to see how those providers recurse through records, arrays, and datatypes.

Next: Operator overloading.