Proposal: Default Implementations (def impl)
Status: Approved Author: Eric (with Claude) Created: 2026-01-29 Approved: 2026-01-29
Summary
Introduce def impl syntax to declare a default implementation for a trait. When a module exports both a trait and its def impl, importing the trait automatically binds the default implementation to the trait name.
// Definition
pub trait Http {
@get (url: str) -> Result<Response, Error>
@post (url: str, body: str) -> Result<Response, Error>
}
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...
@post (url: str, body: str) -> Result<Response, Error> = ...
}
// Usage - no with...in needed
use std.net.http { Http }
@fetch () -> Result<str, Error> uses Http =
Http.get(url: "https://api.example.com/data")
Motivation
The Problem
Currently, using a capability requires verbose with...in boilerplate:
use std.net.http { Http, StdHttp }
@main () -> void =
with Http = StdHttp {} in
{
let user = fetch_user(id: 1)?
print(msg: `Got user: {user.name}`)
}
Issues:
- Verbose — Every entry point needs
with...infor each capability - Confusing types — Users must know about
StdHttp,RealHttp, etc. - Documentation burden — Guide must explain trait vs implementation distinction
- Boilerplate — The common case (use the standard impl) requires the most code
The Solution
def impl provides a default that’s automatically bound when importing:
use std.net.http { Http }
@main () -> void =
{
let user = fetch_user(id: 1)?, // Http "just works"
print(msg: `Got user: {user.name}`)
}
- No
StdHttptype to know about - No
with...infor the common case with...instill available for testing/custom config
Design Philosophy
This is “classes without classes” — compositional behavior without inheritance:
| OOP Concept | Ori Equivalent |
|---|---|
| Interface | trait |
| Default class | def impl |
| Dependency injection | with...in override |
| Constructor | Importing brings the default |
Benefits of traditional classes (encapsulation, polymorphism, defaults) without the baggage (inheritance hierarchies, hidden state, this confusion).
Design
Syntax
def_impl = "def" "impl" trait_name "{" { impl_method } "}" .
impl_method = "@" identifier "(" parameters ")" "->" type "=" expression .
The def keyword marks this as the default implementation. No for Type clause — the implementation is anonymous.
Visibility
def impl can be pub or private:
pub def impl Http { ... } // Exported with trait
def impl Http { ... } // Module-internal only
Semantics
- One default per trait per module — Multiple
def impl Httpin the same module is a compile error - One default per trait globally is NOT enforced — Different modules can have different defaults; the importer chooses
- Import binds the default —
use mod { Trait }bindsTraitto the default if one exists with...inoverrides — Can always override with explicitwith Trait = other in- Same-module usage — Within the defining module, the trait name resolves to the default
Name Resolution
When resolving a capability name:
- Check for
with...inbinding (innermost first) - Check for imported default
- Check for module-local
def impl - Error: capability not provided
Method Dispatch
Http.get(url: "...") calls the method on the bound default. This is identical to how with Http = impl in Http.get(...) works today.
No Anonymous Type Needed
The def impl doesn’t create a named type. It’s an anonymous implementation. Users never see a type name — they just use the trait name.
If state is needed, the implementation can use module-level bindings. This is the recommended pattern for configurable defaults:
// Configuration via module-level immutable bindings
let $timeout = 30s
let $base_url = "https://api.example.com"
pub def impl Http {
@get (url: str) -> Result<Response, Error> =
__http_get(url: $base_url + url, timeout: $timeout)
}
This pattern keeps the default stateless while allowing configuration through module-level constants.
Examples
Standard Library Capability
// std/net/http/mod.ori
pub type Response = {
status: int,
headers: {str: str},
body: str,
}
pub trait Http {
@get (url: str) -> Result<Response, Error>
@post (url: str, body: str) -> Result<Response, Error>
@put (url: str, body: str) -> Result<Response, Error>
@delete (url: str) -> Result<Response, Error>
}
pub def impl Http {
@get (url: str) -> Result<Response, Error> =
__http_request(method: "GET", url: url, body: "")
@post (url: str, body: str) -> Result<Response, Error> =
__http_request(method: "POST", url: url, body: body)
@put (url: str, body: str) -> Result<Response, Error> =
__http_request(method: "PUT", url: url, body: body)
@delete (url: str) -> Result<Response, Error> =
__http_request(method: "DELETE", url: url, body: "")
}
User Code
use std.net.http { Http, Response }
@fetch_user (id: int) -> Result<User, Error> uses Http =
{
let response = Http.get(url: `https://api.example.com/users/{id}`)?
parse_user(json: response.body)
}
@main () -> void =
{
let user = fetch_user(id: 1)?
print(msg: `Got user: {user.name}`)
}
No with...in needed. Http is automatically bound to the default from std.net.http.
Testing with Mocks
type MockHttp = { responses: {str: str} }
impl MockHttp: Http {
@get (self, url: str) -> Result<Response, Error> =
match self.responses[url] {
Some(body) -> Ok(Response { status: 200, headers: {}, body: body }),
None -> Err(Error.new(msg: `Not found: {url}`)),
}
@post (self, url: str, body: str) -> Result<Response, Error> =
Ok(Response { status: 201, headers: {}, body: "" })
// ... other methods
}
@test_fetch_user tests @fetch_user () -> void =
with Http = MockHttp { responses: {
"https://api.example.com/users/1": `{"id": 1, "name": "Alice"}`,
}} in {
let result = fetch_user(id: 1)
assert_ok(result: result)
}
The mock overrides the default via with...in.
Custom Configuration
When you need custom config, use with...in:
type ConfiguredHttp = { timeout: Duration, base_url: str }
impl ConfiguredHttp: Http {
@get (self, url: str) -> Result<Response, Error> =
__http_request(
method: "GET",
url: self.base_url + url,
timeout: self.timeout,
)
// ...
}
@main () -> void =
with Http = ConfiguredHttp { timeout: 5s, base_url: "https://api.example.com" } in
{
let user = fetch_user(id: 1)?
print(msg: `Got user: {user.name}`)
}
Multiple Capabilities in One Module
A module can provide defaults for multiple related traits:
// std/net/mod.ori
pub trait Http { ... }
pub def impl Http { ... }
pub trait Tcp { ... }
pub def impl Tcp { ... }
Both defaults are exported. Importers get whichever they import:
use std.net { Http } // Just Http default
use std.net { Http, Tcp } // Both defaults
Name Collision
If you import a trait with a default but already have that name defined:
pub trait Http { ... }
pub def impl Http { ... }
use other.module { Http } // ERROR: Http already defined
Fix with rename:
use other.module { Http as OtherHttp } // OK
Constraints
One Default Per Trait Per Module
pub def impl Http { ... }
pub def impl Http { ... } // ERROR: duplicate default for Http
Default Must Implement All Methods
pub trait Http {
@get (url: str) -> Result<Response, Error>
@post (url: str, body: str) -> Result<Response, Error>
}
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...
// ERROR: missing implementation for @post
}
No self Parameter
Unlike impl Type: Trait, def impl methods don’t have self:
// Regular impl - has self
impl MyHttp: Http {
@get (self, url: str) -> Result<Response, Error> = ...
}
// def impl - no self (stateless)
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...
}
If state is needed, use module-level bindings or create a typed impl instead.
Implementation
Lexer
Add def as a keyword.
Parser
Parse def impl Trait { ... } as a new AST node:
pub struct DefImpl {
pub visibility: Visibility,
pub trait_name: Name,
pub methods: Vec<ImplMethod>,
pub span: Span,
}
IR
Add DefImpl to module items. Track which traits have defaults in module metadata.
Module System
When exporting a trait:
- Check if module has
def implfor that trait - If so, mark the export as “has default”
When importing a trait:
- If source module has a default, create a binding for the trait name to the default impl
- The binding is a value that can be used for method dispatch
Type Checker
- Verify
def implimplements all trait methods - Verify method signatures match trait
- Verify no duplicate
def implfor same trait in module
Evaluator
When evaluating capability method call (e.g., Http.get(...)):
- Look up
Httpin scope - If bound to a
def impl, dispatch to that implementation - If bound via
with...in, dispatch to that implementation - Error if not bound
Spec Changes Required
08-declarations.md
Add new section:
## Default Implementations
A default implementation provides the standard behavior for a trait:
\`\`\`ori
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...
}
\`\`\`
When a module exports both a trait and its `def impl`, importing the trait automatically binds the default.
12-modules.md
Update imports section:
### Default Bindings
When importing a trait that has a `def impl` in its source module, the default implementation is automatically bound to the trait name:
\`\`\`ori
use std.net.http { Http } // Http bound to default impl
Http.get(url: "...") // Uses default
\`\`\`
Override with `with...in`:
\`\`\`ori
with Http = MockHttp {} in
Http.get(url: "...") // Uses mock
\`\`\`
14-capabilities.md
Update to use def impl pattern:
## Capability Traits
Capabilities are traits with default implementations:
\`\`\`ori
pub trait Http {
@get (url: str) -> Result<Response, Error>
@post (url: str, body: str) -> Result<Response, Error>
}
pub def impl Http {
@get (url: str) -> Result<Response, Error> = ...
@post (url: str, body: str) -> Result<Response, Error> = ...
}
\`\`\`
Import the trait to use the default:
\`\`\`ori
use std.net.http { Http }
@fetch () -> Result<str, Error> uses Http =
Http.get(url: "...")
\`\`\`
grammar.ebnf
Add production:
def_impl = "def" "impl" identifier "{" { impl_method } "}" .
Migration
Guide Updates
The capabilities guide (docs/guide/13-capabilities.md) should be significantly simplified:
- Remove “How Capabilities Connect to the Real World” section about runtime-provided implementations
- Remove references to
RealHttp,StdHttp, etc. - Show simple usage: import trait, use it
- Show override pattern for testing
Stdlib Updates
Update capability modules to use def impl:
std.net.http—def impl Httpstd.fs—def impl FileSystemstd.time—def impl Clockstd.math.rand—def impl Randomstd.log—def impl Loggerstd.env—def impl Env
Alternatives Considered
1. Namespace-based approach
use std.net.http as Http
Http.get(url: "...")
Rejected: Too implicit. Not clear Http is a capability. Conflates modules with capability instances.
2. Explicit default import syntax
use std.net.http { Http: default }
Rejected: Unnecessary verbosity. If there’s a default, importing the trait should give it to you.
3. Module-level with bindings
with Http = std.net.http.default
@fetch () -> ... uses Http = ...
Rejected: Still requires knowing about a default export. More boilerplate than def impl.
4. Keep StdHttp pattern
use std.net.http { Http, StdHttp }
with Http = StdHttp {} in ...
Rejected: This is what we have today. It’s verbose and confusing.
Summary
| Aspect | Decision |
|---|---|
| Syntax | def impl Trait { ... } |
| Keyword | def (short for default) |
| Visibility | pub def impl or def impl |
| One per trait per module | Yes (compile error otherwise) |
| Global uniqueness | No (different modules can have different defaults) |
| Import behavior | Importing trait binds default automatically |
| Override | with Trait = other in still works |
| Self parameter | No (stateless default) |
This proposal eliminates capability boilerplate while maintaining explicitness and testability.