-
Notifications
You must be signed in to change notification settings - Fork 566
Proposal: Using Interface for Keyword Arguments
This proposal extends XGo's keyword arguments system with a fourth mechanism: interface-based keyword arguments. This mechanism enables fluent, chainable parameter construction via a builder-pattern interface, while preserving full type safety, IDE discoverability, and seamless fallback for unrecognized keys.
XGo already supports three keyword argument strategies:
| Strategy | Best For |
|---|---|
map[string]any? |
Dynamic/extensible parameter sets |
Tuple (fields...)?
|
Fixed fields, compile-time validation |
Struct *T?
|
Go compatibility, reflection, struct tags |
However, none of these naturally fits a common pattern in Go ecosystems: builder-style interfaces used by SDK and library authors (e.g., AI clients, HTTP clients, RPC frameworks). These interfaces look like:
// With optional Set escape hatch
type CompletionParams interface {
Set(name string, val any) CompletionParams // optional
MaxOutputTokens(n int64) CompletionParams
System(prompt ...string) CompletionParams
// ...
}Callers today must write verbose, repetitive chains:
client.Complete(ctx, t.CompletionParams().MaxOutputTokens(1024).System("You are helpful.").Set("topP", 0.9))Interface-based keyword arguments allow this to be written naturally as:
client.Complete ctx, maxOutputTokens = 1024, system = "You are helpful.", topP = 0.9Three elements must be present for the compiler to recognize interface-based keyword arguments:
A named interface that has one or more typed setter methods, each returning the same interface type (Self). Optionally, it may also include a Set(name string, val any) Self fallback method to handle unrecognized keyword names at runtime.
// Without Set — only typed keywords are accepted; unknown keywords are a compile-time error
type CompletionParams interface {
MaxOutputTokens(int64) CompletionParams
System(prompt ...string) CompletionParams
Temperature(float64) CompletionParams
}
// With Set — typed keywords plus an escape hatch for arbitrary keys
type CompletionParams interface {
Set(name string, val any) CompletionParams
MaxOutputTokens(int64) CompletionParams
System(prompt ...string) CompletionParams
Temperature(float64) CompletionParams
}A method on the receiver type whose name matches the interface type name exactly (case-sensitive), takes no arguments, and returns the interface:
type Client struct { /* ... */ }
func (c *Client) CompletionParams() CompletionParamsThe compiler uses this factory to construct the initial (zero) params object before chaining setter calls.
A function or method whose last non-variadic parameter is of the interface type. The parameter may be declared as either required or optional (?):
// Required — callers must always supply keyword arguments or an explicit params value
func (c *Client) Complete(ctx context.Context, params CompletionParams) Response
// Optional — callers may omit all keyword arguments entirely
func (c *Client) Complete(ctx context.Context, params CompletionParams?) ResponseWhen the compiler detects a keyword-argument call to such a function, it synthesizes the params object automatically. When the parameter is optional and no keyword arguments are supplied, see §5 for the special nil passthrough rule.
Given a keyword key = value at the call site, the compiler resolves it as follows:
Search the interface for a method whose name, compared case-insensitively, matches key:
| Call-site keyword | Interface method matched |
|---|---|
maxOutputTokens = 1024 |
MaxOutputTokens(int64) |
system = "hello" |
System(prompt ...string) |
temperature = 0.7 |
Temperature(float64) |
The compiler verifies that the value type is assignable to the method's parameter type. A type mismatch is a compile-time error.
If no method matches the keyword name, the compiler checks whether the interface declares a Set(name string, val any) Self method:
-
Setis present — the compiler emits.Set("key", value), preserving the exact casing of the key written at the call site:topP = 0.9 → .Set("topP", 0.9) -
Setis absent — the unrecognized keyword is a compile-time error: the interface does not support unknown keyword<key>.
When a matched method has a variadic parameter (e.g., System(prompt ...string)), the keyword value may be:
- A single scalar — passed as the sole variadic element.
- A slice/array literal — each element is spread into the variadic.
// Single value
complete system = "You are helpful."
// → params.System("You are helpful.")
// Multiple values via slice literal
complete system = ["You are helpful.", "Answer concisely."]
// → params.System("You are helpful.", "Answer concisely.")// Interface with Set — accepts both typed and arbitrary keyword arguments
type CompletionParams interface {
Set(name string, val any) CompletionParams
MaxOutputTokens(int64) CompletionParams
System(prompt ...string) CompletionParams
}
// Interface without Set — only typed keyword arguments are accepted
type GenerationParams interface {
MaxOutputTokens(int64) GenerationParams
Temperature(float64) GenerationParams
}
type Client struct{}
func (c *Client) CompletionParams() CompletionParams
func (c *Client) GenerationParams() GenerationParams
func (c *Client) Complete(ctx context.Context, params CompletionParams?) Response
func (c *Client) Generate(ctx context.Context, params GenerationParams?) Responsevar c Client
// CompletionParams has Set — typed keys and unknown keys both work
c.Complete ctx,
maxOutputTokens = 1024,
system = "You are helpful.",
topP = 0.9 // unknown key, routed to Set
c.Complete ctx,
maxOutputTokens = 512,
system = ["Be concise.", "Use bullet points."],
topK = 40 // unknown key, routed to Set
// GenerationParams has no Set — only typed keys are allowed
c.Generate ctx,
maxOutputTokens = 2048,
temperature = 0.7
// No keyword arguments — params is optional, so nil is passed directly
c.Complete ctx
c.Generate ctxvar c Client
c.Complete(ctx,
c.CompletionParams().
MaxOutputTokens(1024).
System("You are helpful.").
Set("topP", 0.9),
)
c.Complete(ctx,
c.CompletionParams().
MaxOutputTokens(512).
System("Be concise.", "Use bullet points.").
Set("topK", 40),
)
c.Generate(ctx,
c.GenerationParams().
MaxOutputTokens(2048).
Temperature(0.7),
)
// nil — factory is NOT called
c.Complete(ctx, nil)
c.Generate(ctx, nil)When the interface parameter is declared with ?, it is optional. The compiler's behavior then depends on whether the caller supplies any keyword arguments:
If the call site provides zero keyword arguments for the optional interface parameter, the compiler passes nil directly. The factory method is not called.
// Declaration
func (c *Client) Complete(ctx context.Context, params CompletionParams?) Response
// Call with no keyword arguments
c.Complete ctx
// Desugars to:
c.Complete(ctx, nil)This is consistent with XGo's general optional-parameter rule: omitted optional parameters default to their type's zero value, and the zero value of an interface type is nil. Calling the factory to produce an empty params object would be semantically wrong (it may allocate, carry hidden state, or trigger side effects), so nil is the correct passthrough.
As soon as at least one keyword argument is present, the compiler calls the factory and builds the chain normally:
c.Complete ctx, maxOutputTokens = 512
// Desugars to:
c.Complete(ctx, c.CompletionParams().MaxOutputTokens(512))| Declared as | Keyword args at call site | Compiler emits |
|---|---|---|
CompletionParams (required) |
one or more | c.CompletionParams().Method(...)... |
CompletionParams (required) |
none | compile-time error (missing argument) |
CompletionParams? (optional) |
one or more | c.CompletionParams().Method(...)... |
CompletionParams? (optional) |
none | nil |
Library authors should document the meaning of a nil params value. The idiomatic pattern is to treat nil as "use all defaults":
func (c *Client) Complete(ctx context.Context, params CompletionParams?) Response {
if params == nil {
// All defaults apply — no params object needed
}
// ...
}Keyword arguments for an interface parameter must appear after all positional arguments, consistent with XGo's existing keyword argument convention:
// Positional ctx first, then keyword arguments
c.Complete ctx, maxOutputTokens = 1024, system = "hello"
// Also valid: no keyword arguments (pass params explicitly)
c.Complete(ctx, myParams)Keyword arguments and an explicit positional value for the same interface parameter cannot be mixed in a single call — this is a compile-time error.
The factory method is resolved by the following algorithm:
- Look at the receiver of the call (e.g.,
cforc.Complete(...)). - Search the receiver's method set for a method named exactly the same as the interface type name (e.g.,
CompletionParams). - The method must have no parameters and return exactly the interface type.
- If no such method is found on the direct receiver, the compiler reports an error indicating that no factory method was found for the interface keyword parameter.
Rationale: Tying the factory method name to the interface type name creates a predictable, greppable convention. Library authors advertise the factory by naming it after the interface.
| Situation | Error |
|---|---|
| Keyword value type incompatible with matched method parameter | Compile-time type mismatch error |
| No factory method found on receiver | Compile-time error: no factory method <InterfaceName>() found on receiver <Type>
|
Interface has no Set method and keyword has no matching method |
Compile-time error: interface does not support unknown keyword <key>
|
| Keyword arguments mixed with explicit interface positional argument | Compile-time error: cannot mix keyword and positional arguments for the same parameter |
| Matched method has multiple non-variadic parameters | Compile-time error: method <n> requires exactly one argument |
| Required interface parameter with no keyword arguments and no positional value | Compile-time error: missing argument for required parameter <n>
|
| Matched method has multiple non-variadic parameters | Compile-time error: method <Name> requires exactly one argument |
| Feature | Map | Tuple | Struct | Interface |
|---|---|---|---|---|
| Type-safe known keys | ✗ | ✓ | ✓ | ✓ |
| Unknown keys supported | ✓ | ✗ | ✗ | ✓ (opt-in via Set) |
| Variadic value per key | ✗ | ✗ | ✗ | ✓ |
| Builder/SDK compatibility | ✗ | ✗ | ✗ | ✓ |
| Compile-time validation | ✗ | ✓ | ✓ | ✓ (known keys) |
Optional with nil passthrough |
✓ | ✓ | ✓ | ✓ |
| Runtime reflection needed | ✗ | ✗ | ✓ | ✗ |
| Go codebase interoperability | Limited | Limited | ✓ | ✓ |
- Use interface keyword arguments when integrating with SDK-style builder interfaces (AI clients, HTTP clients, RPC frameworks).
- Name the factory method after the interface type to make the convention explicit and tooling-friendly.
-
Omit
Setfor strict interfaces where only the declared typed parameters should be accepted; unrecognized keywords will be caught at compile time. -
Add
Setas an opt-in escape hatch when the interface needs to support experimental, deprecated, or rarely used parameters that don't warrant a dedicated typed method. -
Prefer typed methods over
Setfor all performance-critical or frequently used parameters, as they provide compile-time checking. -
Document the
Setkey namespace in your library's documentation whenSetis present, as those keys are invisible to the compiler.
Interface-based keyword arguments bring XGo's expressive call-site syntax to the builder-pattern interfaces that are ubiquitous in modern Go libraries. The design follows three principles:
- Zero new runtime cost — all synthesis happens at compile time; the emitted code is idiomatic Go method chaining.
-
Progressive disclosure — known keys are always type-checked; unknown keys fall back to
Setwhen the interface opts in, or are rejected at compile time when it does not. - Convention over configuration — the factory method naming rule makes the feature self-documenting and easy to discover.