Skip to content

Proposal: Using Interface for Keyword Arguments

xushiwei edited this page Mar 23, 2026 · 3 revisions

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.


Motivation

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.9

Design

1. Anatomy of an Interface Keyword Argument

Three elements must be present for the compiler to recognize interface-based keyword arguments:

1.1 The Params Interface

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
}

1.2 The Factory Method

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() CompletionParams

The compiler uses this factory to construct the initial (zero) params object before chaining setter calls.

1.3 The Target Function

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?) Response

When 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.


2. Keyword-to-Method Mapping

Given a keyword key = value at the call site, the compiler resolves it as follows:

Step 1 — Exact match (case-insensitive)

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.

Step 2 — Fallback via Set (if present)

If no method matches the keyword name, the compiler checks whether the interface declares a Set(name string, val any) Self method:

  • Set is 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)
    
  • Set is absent — the unrecognized keyword is a compile-time error: the interface does not support unknown keyword <key>.


3. Handling Variadic Setter Methods

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.")

4. Full Desugaring Example

Declaration

// 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?) Response

XGo source

var 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 ctx

Desugared equivalent

var 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)

5. Optional Interface Parameters and nil Passthrough

When the interface parameter is declared with ?, it is optional. The compiler's behavior then depends on whether the caller supplies any keyword arguments:

5.1 No keyword arguments supplied → pass nil

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.

5.2 One or more keyword arguments supplied → call factory and chain

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))

5.3 Summary of optional-parameter dispatch

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

5.4 Callee-side handling of 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
    }
    // ...
}

6. Mixing with Positional Arguments

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.


7. Factory Method Lookup Rules

The factory method is resolved by the following algorithm:

  1. Look at the receiver of the call (e.g., c for c.Complete(...)).
  2. Search the receiver's method set for a method named exactly the same as the interface type name (e.g., CompletionParams).
  3. The method must have no parameters and return exactly the interface type.
  4. 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.


8. Error Cases

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

9. Comparison with Existing Strategies

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

10. Recommended Usage Guidelines

  • 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 Set for strict interfaces where only the declared typed parameters should be accepted; unrecognized keywords will be caught at compile time.
  • Add Set as 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 Set for all performance-critical or frequently used parameters, as they provide compile-time checking.
  • Document the Set key namespace in your library's documentation when Set is present, as those keys are invisible to the compiler.

11. Summary

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:

  1. Zero new runtime cost — all synthesis happens at compile time; the emitted code is idiomatic Go method chaining.
  2. Progressive disclosure — known keys are always type-checked; unknown keys fall back to Set when the interface opts in, or are rejected at compile time when it does not.
  3. Convention over configuration — the factory method naming rule makes the feature self-documenting and easy to discover.

Clone this wiki locally