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. Adopt an unassociated provider with with extensions Module.Extension do ... end around the smallest expression or function body that owns the policy. Non-associated declarations in the same module are inert as well. A private extension that is never associated, exported, or referenced explicitly is an unused-extension error: an inert provider is almost always a forgotten extensions clause rather than intent, so associate it with its datatype, interface, or type alias, export it, or reference it with with extensions, a named value.Provider.method() call, or an explicit ?{Question: Provider} witness. The scope is lexical, value-producing, and is not re-exported or dynamically inherited by called functions.

For a canonical implementation, prefer associating the provider with its nominal owner (or with a transparent alias view over that owner). A lexical scope is primarily for a consumer-owned alternate policy, especially when the choice affects operators or omitted witnesses and therefore cannot be reduced to one named call.

For one call, name the provider directly after the receiver:

rendered = value.Provider.DebugMethods.render()

This selects Provider.DebugMethods without activating it for value.render() or any surrounding expression. Only members declared with method may use this form. Extension type arguments are inferred from the receiver, and declared prerequisites use ordinary extension resolution. A provider named across a module boundary must be exported. Use its explicit extension-value form when a call must override prerequisite providers.

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
func compare_with_zero(value : Int) -> Int do
with extensions Provider.IntCompare do
value.compare(0)
end
end
func main() -> Unit do
Debug.trace(compare_with_zero(1));
end

The lexical scope must name an exported, unassociated provider when crossing a module boundary. Multiple providers may be listed with commas; repeating a provider is idempotent. Associated providers, such as an extension listed after a datatype’s extensions clause, do not need lexical adoption at each call site. A lexical interface extension can override an associated default only when its @preferred_over graph covers the complete associated survivor frontier; contextual witnesses always remain stronger.

@preferred_over(DefaultProvider) uses a bare extension name. The target is looked up in the current module’s extension namespace and among providers associated with the source extension’s subject and interface. A provider from another module may remain private when its public datatype, type alias, or interface association exposes the provider identity. This does not make that provider name usable in with extensions, a named call, or an explicit witness answer. Qualified preference targets such as @preferred_over(Module.Provider) are not supported yet.

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. When the selected provider must remain part of a value’s static identity, use a witness-indexed type instead of storing the dictionary as an ordinary field.

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: Witness-indexed types.