-
Notifications
You must be signed in to change notification settings - Fork 563
Description
Abstract
This proposal suggests introducing tuple types in XGo as syntactic sugar to simplify multi-value returns, channel transmissions, and map value handling. Tuples are essentially a syntactic simplification of anonymous structs, providing clearer multi-value semantics.
Motivation
Although Go supports multi-value returns, it lacks unified multi-value type representations in the following scenarios:
- Transmitting multiple values through channels: Currently requires defining structs or using multiple channels
- Storing multiple values in maps: Requires explicit struct type definitions
- Functional programming: Lacks lightweight value composition mechanisms
Introducing tuples can provide more flexible multi-value handling while maintaining Go's simplicity.
Design Specification
Basic Equivalence Relations
Tuple types are syntactic sugar for the following structs:
() ≡ struct{}
(T) ≡ T
(name T) ≡ T
(T0, T1, ..., TN) ≡ struct{ X_0 T0; X_1 T1; ...; X_N TN }
(name0 T0, name1 T1, ..., nameN TN) ≡ struct{ X_0 T0; X_1 T1; ...; X_N TN }Core Principles:
- Empty tuple
()is equivalent to an empty struct - Single-element tuples degenerate to the type itself (avoiding nesting)
(100)is completely equivalent to100(T)and(name T)both degenerate toT
- Multi-element tuples map to struct fields named by ordinal numbers
- Names in named tuples are only valid during compilation; the runtime structure still uses ordinal fields
- Type shorthand: Consecutive fields of the same type can be abbreviated, following Go function parameter syntax
(x int, y int)can be written as(x, y int)(a, b, c string, d int)is equivalent to(a string, b string, c string, d int)
Compile-Time Semantics of Named Fields
Critical Specification: Named fields in tuples (such as name0 T0) are compile-time aliases that are uniformly converted to ordinal fields after compilation.
Naming Rules
// Declaration
type Point (x int, y int)
// Can be abbreviated as
type Point (x, y int)
// Equivalent runtime structure
type Point struct {
X_0 int // corresponds to x
X_1 int // corresponds to y
}
// Compile-time equivalence
v.x ≡ v.X_0
v.y ≡ v.X_1Type Shorthand Syntax
Following Go's function parameter syntax, consecutive fields of the same type can share a type declaration:
// Full form
type Point3D (x int, y int, z int)
// Abbreviated form (recommended)
type Point3D (x, y, z int)
// Both are equivalent to
type Point3D struct {
X_0 int // x
X_1 int // y
X_2 int // z
}
// Mixed types
type Person (firstName, lastName string, age int, active bool)
// Equivalent to
type Person (firstName string, lastName string, age int, active bool)
// Complex example
type Record (id int, name, description string, count, total int, valid bool)
// Expands to
type Record (id int, name string, description string, count int, total int, valid bool)Compile-Time Transformation Examples
// Source code
type Result (value int, err error)
func process() Result {
return Result{42, nil} // Positional construction
// or
return Result{value: 42, err: nil} // Named field construction
}
result := process()
println(result.value) // Compile-time conversion to result.X_0
println(result.err) // Compile-time conversion to result.X_1
// Equivalent compiled code
type Result struct {
X_0 int // value
X_1 error // err
}
func process() Result {
return Result{X_0: 42, X_1: nil}
// Both construction styles compile to the same thing
}
result := process()
println(result.X_0)
println(result.X_1)Field Access Syntax
Access Semantics
-
Named access (for named tuples only):
type Person (name string, age int) p := Person{"Alice", 30} println(p.name) // Compiler converts to p.X_0 println(p.age) // Compiler converts to p.X_1
-
Numeric index access (for all tuples):
// Anonymous tuple v := (10, "hello", true) a := v.0 // 10, compiles to v.X_0 b := v.1 // "hello", compiles to v.X_1 c := v.2 // true, compiles to v.X_2 // Named tuple - numeric index still works type Person (name string, age int) p := Person{"Alice", 30} println(p.0) // "Alice", compiles to p.X_0 println(p.1) // 30, compiles to p.X_1
-
Mixed access:
type Data (x, y, z int) d := Data{1, 2, 3} // Can mix named and numeric index access sum := d.x + d.1 + d.z // Equivalent to d.X_0 + d.X_1 + d.X_2
-
Ordinal access (always valid but not recommended):
v := (10, "hello", true) a := v.X_0 // Valid but not recommended b := v.X_1 // Valid but not recommended c := v.X_2 // Valid but not recommended
Note: Direct ordinal field access (
X_0,X_1, etc.) is always valid but not encouraged. Use numeric indices (v.0,v.1) or named fields instead for better readability.
Reflection Behavior
Since names are only valid at compile time, reflection can only see ordinal fields:
type Point (x, y int)
p := Point(3, 4)
t := reflect.TypeOf(p)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
println(field.Name) // Output: "X_0", "X_1"
}
// Reflection cannot access compile-time names like "x", "y"Handling Name Conflicts
Ordinal field names X_0, X_1, ... are reserved and cannot be used as user-defined field names:
// Invalid: cannot use reserved ordinal names
type Bad (X_0 int, X_1 string) // Compile error
// Invalid: name conflicts with ordinal
type Invalid (x int, X_1 string) // Compile error
// Valid: all user-defined names
type Good (x int, y string) // OK
// Valid: all ordinals (anonymous tuple)
type Good (int, string) // OKType Compatibility
Two tuple types can be assigned to each other when structurally equivalent, regardless of naming differences:
type Point (x, y int)
type Coordinate (lng, lat int)
var p Point = Point{3, 4}
var c Coordinate = Coordinate(p) // OK: same structure (int, int)
// At runtime, both are struct{ X_0 int; X_1 int }However, tuples with different structures are incompatible:
type Point2D (x, y int)
type Point3D (x, y, z int)
var p2 Point2D = Point2D{1, 2}
var p3 Point3D = Point3D(p2) // Compile error: different structureSingle-Element Tuple Equivalence
Single-element tuples completely degenerate to their base type:
type Wrapper (value int)
var w Wrapper = 100 // OK: (value int) ≡ int
var x int = Wrapper{42} // OK: can assign back to int
// Parenthesized expressions are just regular expressions
y := (100) // y is int, not a tuple
z := (100 + 200) // z is int, value 300
// All of these are completely equivalent
var a int = 42
var b Wrapper = 42
var c (int) = 42
var d (value int) = 42
// a, b, c, d are all just int at runtimeType Declarations
// Channel type
var c chan (int, string, bool)
// Equivalent to: var c chan struct{ X_0 int; X_1 string; X_2 bool }
// Map type
var m map[string](int, error)
// Equivalent to: var m map[string]struct{ X_0 int; X_1 error }
// Named tuple (compile-time names)
type Result (value int, err error)
// Equivalent to: type Result struct{ X_0 int; X_1 error }
// Access: result.value compiles to result.X_0
// Slice type
var pairs [](string, int)Value Construction
// Anonymous construction (by position)
t := (42, "hello", true)
// Equivalent to: t := struct{...}{X_0: 42, X_1: "hello", X_2: true}
// Named tuple construction using standard struct literal syntax
type Point (x, y int)
p1 := Point{3, 4} // Positional
p2 := Point{x: 3, y: 4} // Named fields (compiles to Point{X_0: 3, X_1: 4})
p3 := Point{y: 4, x: 3} // Order doesn't matter with named fields
// Anonymous tuple literal
pairs := [](string, int){
("a", 1), // Parentheses are optional for clarity
("b", 2),
}
// Standard Go struct literal syntax works because tuples ARE structs
type Person (name string, age int)
alice := Person{name: "Alice", age: 30} // Compiles to Person{X_0: "Alice", X_1: 30}
bob := Person{"Bob", 25} // Positional also worksFunction-Style Construction Syntax
Overview
Tuple types can be viewed as constructor functions. For a named tuple type, function-style calls provide a concise way to construct tuple values.
Basic Syntax
// Named tuple type
type Point (x, y int)
// Function-style construction with positional arguments
p1 := Point(3, 4)
// Equivalent to: p1 := Point{3, 4}
// Function-style construction with kwargs (order-independent)
p2 := Point(y = 4, x = 3)
// Compiles to: p2 := Point{x: 3, y: 4}
// NOT supported: colon syntax in function-style
// p3 := Point(x: 3, y: 4) // ✗ Compile error
// NOT supported: mixed positional + kwargs
// p4 := Point(3, y = 4) // ✗ Compile errorImportant: Function-style construction only supports:
- Positional arguments:
Point(3, 4) - Kwargs (all fields named with
=):Point(y = 4, x = 3)
Mixing positional and kwargs is not allowed.
Comparison with Struct Literal Syntax
type Result (value int, err error)
// Function-style positional
r1 := Result(42, nil)
// Equivalent to: Result{42, nil}
// Function-style kwargs (order independent)
r2 := Result(err = nil, value = 42)
// Compiles to: Result{value: 42, err: nil}
// Struct literal with positional
r3 := Result{42, nil}
// Struct literal with named fields (any order)
r4 := Result{err: nil, value: 42}
// r1 and r3 are equivalent
// r2 and r4 are equivalentKeyword Arguments (kwargs) Support
Kwargs use the = syntax and allow fields to be specified in any order:
type Point (x, y int)
// Valid function-style constructions:
p1 := Point(3, 4) // ✓ All positional
p2 := Point(y = 4, x = 3) // ✓ All kwargs (order doesn't matter)
p3 := Point(x = 3, y = 4) // ✓ All kwargs (same result as p2)
// Invalid - cannot mix positional and kwargs:
// p4 := Point(3, y = 4) // ✗ Compile error
type Color (r, g, b, a uint8)
// All kwargs - order independent
c1 := Color(r = 255, g = 0, b = 0, a = 255)
c2 := Color(a = 255, r = 255, b = 0, g = 0) // Same result
// This is especially useful for tuples with many fields
type Config (host string, port int, timeout time.Duration, retry int, debug bool)
// Can specify fields in any order with kwargs
cfg := Config(
debug = true,
host = "localhost",
timeout = 30*time.Second,
port = 8080,
retry = 3,
)Rules for function-style construction:
- Either all positional OR all kwargs - no mixing allowed
- Kwargs use
=for assignment - Kwargs can appear in any order
- Cannot specify the same field twice
- Supports partial initialization - unspecified fields get zero values
type Point (x, y int)
// Valid
Point(3, 4) // ✓ All positional
Point(y = 4, x = 3) // ✓ All kwargs
Point(x = 3) // ✓ Partial init with kwargs (y = 0)
// Invalid
Point(3, y = 4) // ✗ Cannot mix positional and kwargs
Point(x = 1, x = 2) // ✗ Duplicate field
Point(3) // ✗ Positional args don't support partial initStruct literal syntax remains unchanged:
// Struct literals support both styles as before
Point{3, 4} // Positional
Point{x: 3, y: 4} // Named with : (any order)
Point{x: 3} // Partial initialization (y defaults to 0)Advanced Usage
Nested Tuples
type Point (x, y int)
type Line (start, end Point)
// Function-style for nested construction
line1 := Line(Point(0, 0), Point(10, 10))
// Equivalent to
line1 := Line{Point{0, 0}, Point{10, 10}}
// Or with kwargs (all fields must use kwargs)
line2 := Line(
end = Point(x = 10, y = 10),
start = Point(y = 0, x = 0),
)
// Equivalent to
line2 := Line{
end: Point{x: 10, y: 10},
start: Point{y: 0, x: 0},
}
// NOT supported: mixing positional and kwargs
// line3 := Line(Point(0, 0), end = Point(10, 10)) // ✗ Compile errorAs Function Arguments
type Rectangle (width, height float64)
func area(r Rectangle) float64 {
return r.width * r.height
}
// Direct function-style construction in call
a := area(Rectangle(10.5, 20.0))
// More concise than
a := area(Rectangle{10.5, 20.0})
// Or with kwargs
a := area(Rectangle(height = 20.0, width = 10.5))In Collections
type Student (id int, name string, score float64)
// Function-style in slice literals
students := []Student{
Student(1, "Alice", 95.5),
Student(2, "Bob", 87.0),
Student(score = 92.5, id = 3, name = "Charlie"), // All kwargs
Student(score = 88.0, name = "Diana", id = 4), // All kwargs
}
// In map literals
type Point (x, y int)
coords := map[string]Point{
"origin": Point(0, 0),
"center": Point(50, 50),
"top": Point(y = 100, x = 50), // All kwargs
}Channel and Map Operations
type Message (id int, content string)
var messages chan Message
messages <- Message(101, "hello") // Cleaner than Message{101, "hello"}
// Or with kwargs
messages <- Message(content = "hello", id = 101)
type CacheEntry (data interface{}, expiry time.Time)
cache := make(map[string]CacheEntry)
cache["key"] = CacheEntry(userData, time.Now().Add(5*time.Minute))
// Or with kwargs
cache["key"] = CacheEntry(
expiry = time.Now().Add(5*time.Minute),
data = userData,
)Single-Element Tuples
For single-element tuples that degenerate to their base type, the function-style syntax is just a type conversion:
type Wrapper (value int)
w1 := Wrapper(42) // Type conversion (since Wrapper ≡ int)
w2 := Wrapper{42} // Also valid
// Both are equivalent to
var w3 int = 42Rationale
Why add function-style syntax?
- Consistency: Tuples are value types; constructing them looks like calling a function
- Conciseness:
Point(3, 4)is more compact thanPoint{3, 4} - Familiarity: Similar to constructor syntax in other languages (Python's
Point(x=3, y=4), C++, Rust) - Readability: More natural when passing tuples as arguments or in nested expressions
- Flexibility: kwargs with
=syntax allows specifying fields in any order, improving code clarity for tuples with many fields - Simplicity: Only two modes (all positional or all kwargs) keeps the syntax simple and unambiguous
Design philosophy: Since tuples are conceptually "structured values," constructing them should feel like calling a value constructor. The kwargs support with = syntax provides Python-like keyword argument flexibility while maintaining type safety and simplicity.
Why not allow mixing positional and kwargs?
- Simplicity: Two clear modes are easier to understand than complex mixing rules
- Consistency: Aligns with function call semantics in many languages
- Clarity: Forces explicit choice between positional (when order is obvious) or kwargs (when names matter)
- No ambiguity: Prevents confusion about which fields are being set
Compiler Implementation
The compiler handles function-style construction as follows:
- Syntax Recognition: Distinguish
T(args...)fromT{args...} - Argument Mode Detection:
- Check if all arguments are positional
- Check if all arguments use
=syntax (kwargs) - Reject mixed modes
- Transformation:
- Positional:
T(v1, v2, ...)→T{v1, v2, ...} - Kwargs:
T(f1 = v1, f2 = v2, ...)→T{f1: v1, f2: v2, ...}
- Positional:
- Validation: Apply standard struct literal validation rules
- Code Generation: Generate identical code as struct literals
No runtime overhead or special handling required.
Style Recommendations
When to use function-style:
- Constructing tuple values in expressions
- Passing tuples as function arguments
- Inline construction in collections
- When conciseness improves readability
- When field order is obvious (use positional)
- When field names are important or want partial init (use kwargs)
When to use struct literal style:
- Multi-line initialization with many fields
- When you prefer explicit struct syntax
- When you want the familiar Go struct literal syntax
- In generated code or macros
Example comparison:
type Config (host string, port int, timeout time.Duration)
// Function-style: concise for inline usage
client := NewClient(Config("localhost", 8080, 30*time.Second))
// Function-style with kwargs: clear what each value means
client := NewClient(Config(
host = "localhost",
port = 8080,
timeout = 30*time.Second,
))
// Function-style with kwargs: partial initialization
client := NewClient(Config(
host = "localhost",
port = 8080,
)) // timeout defaults to 0
// Struct literal: clearer for complex initialization
config := Config{
host: getHost(),
port: getPort(),
timeout: calculateTimeout(),
}
client := NewClient(config)Interaction with Type Inference
Function-style syntax works naturally with type inference:
type Point (x, y int)
// Type inferred from map value type
points := map[string]Point{
"a": Point(1, 2),
"b": Point(3, 4),
}
// Type inferred from channel element type
var ch chan Point
ch <- Point(5, 6)
// Type inferred from function parameter
func distance(p Point) float64 { ... }
d := distance(Point(3, 4))Complete Construction Examples
type Point (x, y int)
type Color (r, g, b, a uint8)
type Result (value int, err error)
// Function-style: positional (must specify all fields)
p1 := Point(3, 4)
c1 := Color(255, 0, 0, 255)
r1 := Result(42, nil)
// Function-style: kwargs (any order, supports partial init)
p2 := Point(y = 4, x = 3)
p3 := Point(x = 3) // y defaults to 0
c2 := Color(a = 255, r = 255) // g, b default to 0
r2 := Result(value = 42) // err defaults to nil
// Struct literal: positional
p4 := Point{3, 4}
c3 := Color{255, 0, 0, 255}
r3 := Result{42, nil}
// Struct literal: named (any order, supports partial init)
p5 := Point{y: 4, x: 3}
c4 := Color{a: 255, r: 255}
r4 := Result{value: 42}
p6 := Point{x: 3} // y defaults to 0
// All equivalent at runtime (except partial init examples)!Key points:
- Function-style positional requires all fields
- Function-style kwargs supports partial initialization
- Struct literal supports partial initialization with both positional and named syntax
- Function-style has two clear modes: all positional OR all kwargs
- Struct literal with
:syntax allows any field order
This function-style construction syntax makes XGo tuples feel more like first-class value types while maintaining full compatibility with Go's struct literal syntax.
Tuple Field Access (No Auto-Unpacking)
Important Design Decision: Tuples do NOT support automatic unpacking. All tuple elements must be accessed explicitly via field access.
Channel Operations
var c chan (int, string, bool)
// Receive tuple value
v := <-c // v's type is (int, string, bool)
v, ok := <-c // v is tuple, ok is bool - NO AMBIGUITY
// Access fields using numeric index (recommended)
println(v.0, v.1, v.2)
// Named tuple example
type Message (id int, content string, priority bool)
var mc chan Message
msg := <-mc
msg, ok := <-mc // Clear: msg is Message, ok is bool
// Access using named fields or numeric index
println(msg.id) // Compiles to msg.X_0
println(msg.content) // Compiles to msg.X_1
println(msg.priority) // Compiles to msg.X_2
// Or use numeric index (also works for named tuples)
println(msg.0, msg.1, msg.2)Map Access
var m map[string](int, error)
// Receive tuple value
result := m["key"] // result's type is (int, error)
result, ok := m["key"] // result is tuple, ok is bool - NO AMBIGUITY
// Access fields using numeric index (recommended)
value := result.0 // Compiles to result.X_0
err := result.1 // Compiles to result.X_1
// Assignment
m["key"] = (42, nil)
// Named tuple example
type CacheEntry (data interface{}, expiry time.Time)
var cache map[string]CacheEntry
entry := cache["key"]
entry, ok := cache["key"] // Clear: entry is CacheEntry, ok is bool
// Access using named fields or numeric index
println(entry.data) // Compiles to entry.X_0
println(entry.expiry) // Compiles to entry.X_1
// Or use numeric index (also works for named tuples)
println(entry.0, entry.1)Function Return Values
// Function declaration (existing multi-return syntax remains unchanged)
func divide(a, b int) (int, error) { ... }
// Optional: use tuple type declaration
type DivResult (result int, err error)
func divide(a, b int) DivResult { ... }
// Call and access
result := divide(10, 2)
if result.err != nil { // or result.1 or result.X_1
// handle error
}
println(result.result) // or result.0 or result.X_0
// Traditional multi-return still works
quotient, err := divide(10, 2)Why No Auto-Unpacking?
The original proposal included auto-unpacking syntax like:
v0, v1, v2 := <-c // Auto-unpack channel receive
value, err := m["key"] // Auto-unpack map accessProblem: This creates ambiguity with the comma-ok idiom:
v, ok := <-c // Is this:
// 1. Receive tuple v, check channel status ok?
// 2. Unpack 2-element tuple into v and ok?Solution: Remove auto-unpacking entirely. Users must:
- Receive the complete tuple value
- Explicitly access fields using
.0,.1,.N(numeric index) or.X_0,.X_1,.X_N(ordinal) or named fields
This eliminates all ambiguity while keeping tuple syntax simple and explicit.
Usage Examples
Example 1: Producer-Consumer Pattern
// Define task channel with metadata
type Task (id int, payload string, priority int)
var tasks chan Task
// Producer - using function-style construction
go func() {
tasks <- Task(101, "process data", 1)
tasks <- Task(priority = 2, id = 102, payload = "send email")
}()
// Consumer - receive and access fields
for task := range tasks {
log.Printf("Task %d: %s (priority=%d)",
task.id, task.payload, task.priority)
}
// If you need individual variables, extract using numeric index
for {
task, ok := <-tasks
if !ok {
break
}
id := task.0 // or task.id
payload := task.1 // or task.payload
priority := task.2 // or task.priority
log.Printf("Task %d: %s (priority=%d)", id, payload, priority)
}Example 2: Cache Pattern
type CacheValue (data interface{}, expiry time.Time)
type Cache map[string]CacheValue
func (c Cache) Get(key string) (interface{}, bool) {
entry, ok := c[key] // entry is CacheValue, ok is existence check
if !ok {
return nil, false
}
// Access fields using named fields or numeric index
if time.Now().After(entry.expiry) { // or entry.1
delete(c, key)
return nil, false
}
return entry.data, true // or entry.0
}
func (c Cache) Set(key string, data interface{}, ttl time.Duration) {
c[key] = CacheValue(data, time.Now().Add(ttl))
// Or named fields with colon syntax
c[key] = CacheValue{
data: data,
expiry: time.Now().Add(ttl),
}
// Or kwargs
c[key] = CacheValue(
expiry = time.Now().Add(ttl),
data = data,
)
}
// Usage
cache := make(Cache)
cache.Set("user:123", userData, 5*time.Minute)
if data, ok := cache.Get("user:123"); ok {
// Use data
}Example 3: Concurrent Coordination
type WorkResult (result int, duration time.Duration, err error)
var results chan WorkResult
// Worker - using function-style construction
go func() {
start := time.Now()
res, err := doWork()
results <- WorkResult(res, time.Since(start), err)
// Or with kwargs for clarity
results <- WorkResult(
err = err,
result = res,
duration = time.Since(start),
)
}()
// Collect results - explicit field access
wr, ok := <-results
if !ok {
return
}
// Access fields using named fields or numeric index
if wr.err != nil { // or wr.2
log.Printf("Failed after %v: %v", wr.duration, wr.err)
} else {
log.Printf("Success in %v: %d", wr.duration, wr.result)
}
// Or extract to local variables if needed
result := wr.result // or wr.0
duration := wr.duration // or wr.1
err := wr.err // or wr.2Example 4: Numeric Index Works for Both Anonymous and Named Tuples
// Anonymous tuple in map
var coords map[string](float64, float64)
coords["home"] = (1.234, 5.678)
// Access using numeric index
pos := coords["home"]
x := pos.0 // Compiles to pos.X_0
y := pos.1 // Compiles to pos.X_1
println("Position:", x, y)
// Named tuple - numeric index still works
type Point (x, y float64)
var points map[string]Point
points["home"] = Point(1.234, 5.678)
p := points["home"]
px := p.0 // Compiles to p.X_0, same as p.x
py := p.1 // Compiles to p.X_1, same as p.y
// Can use either numeric index or named fields
println("X:", p.0, "Y:", p.1) // Numeric index
println("X:", p.x, "Y:", p.y) // Named fields
// Channel of anonymous tuples
var pairs chan (string, int)
go func() {
pairs <- ("hello", 42)
pairs <- ("world", 99)
}()
// Receive and access
pair := <-pairs
name := pair.0 // Compiles to pair.X_0
count := pair.1 // Compiles to pair.X_1
println(name, count)Implementation Considerations
Type Inference
// Compiler infers tuple element types from context
m := map[string](int, string){
"a": (1, "one"),
"b": (2, "two"),
}Type Conversion
// Tuples and corresponding structs can convert
type Point (x, y int)
p := Point(3, 4)
s := struct{ X_0, X_1 int }{X_0: 3, X_1: 4}
p2 := Point(s) // Struct to tuple conversion
// Different names, same structure
type Coord (lng, lat int)
c := Coord(p) // OK: both are (int, int)
// Single-element tuple conversion
type ID (value int)
var id ID = 42
var num int = ID(100)Zero Values
var t (int, string, bool)
// Zero value: (0, "", false)
type Point (x, y int)
var p Point
// Zero value: Point{X_0: 0, X_1: 0}
println(p.x, p.y) // 0 0
// Single-element tuple
type Wrapper (value int)
var w Wrapper // Zero value: 0Compiler Implementation Key Points
-
Name Resolution Phase:
- Record mapping from named tuple field names to ordinals
- Record mapping from numeric indices to ordinals (
v.0→v.X_0) - Verify names don't conflict with reserved ordinals
- Handle kwargs (
=syntax) vs struct literal (:syntax) distinction
-
Type Checking Phase:
- Convert all named field accesses to ordinal accesses
- Convert all numeric index accesses to ordinal accesses
- Verify structural compatibility for type conversions
- Transform function-style construction
T(args...)toT{args...} - Validate kwargs ordering and compatibility
-
Code Generation Phase:
- Generate standard struct definitions (only
X_0,X_1, ... fields) - All field accesses converted to ordinal form
- Function-style construction compiled to struct literals
- Generate standard struct definitions (only
-
Debug Information:
- Optional: Preserve original naming information in debug symbols
Compatibility with Existing Features
No Changes to Existing Syntax
- Function multi-return syntax unchanged
- Existing code requires no modifications
- Tuples are optional syntactic sugar
Range Loop
type Student (id int, name string, score float64)
var students []Student
// Traditional range
for i, student := range students {
println(i, student.id, student.name, student.score)
}
// Access fields using names or numeric indices
for _, s := range students {
println(s.0, s.1, s.2) // Numeric index
// Or: println(s.id, s.name, s.score)
}Interface Implementation
type Pair (first int, second int)
func (p Pair) String() string {
return fmt.Sprintf("(%d, %d)", p.first, p.second)
// Or: return fmt.Sprintf("(%d, %d)", p.0, p.1)
}
var _ fmt.Stringer = Pair{}Advantages
- No Ambiguity: Explicit field access eliminates comma-ok ambiguity
- Type Safety: Compile-time type checking
- Zero-Cost Abstraction: Compiles to structs with no runtime overhead
- Code Clarity: Named fields and numeric indices both available
- Progressive Enhancement: Optional feature, doesn't break existing code
- Simple Semantics: Tuples are just structs with convenient syntax
- Flexible Construction: Multiple construction styles (positional, named, kwargs)
- Familiar Syntax: Function-style construction similar to other languages
Potential Issues and Solutions
Issue 1: Conflict with Parenthesized Expressions
x := (42) // Is this a tuple or expression?Solution: Single-element tuples degenerate to base type, so (42) is just 42.
Issue 2: Nested Tuple Readability
var nested chan ((int, string), (bool, error))Solution: Use type aliases
type Request (id int, data string)
type Response (success bool, err error)
var ch chan (Request, Response)Issue 3: Reflection Cannot Access Original Names
Reflection only sees X_0, X_1, etc.
Solution:
- Document this limitation
- Use regular structs if reflection-friendly fields needed
- Optional: Preserve name mapping in debug symbols
Issue 4: More Verbose Than Auto-Unpacking
Without auto-unpacking, code is slightly more verbose:
// Before (with auto-unpack)
v0, v1, v2 := <-c
// After (explicit access)
v := <-c
v0, v1, v2 := v.0, v.1, v.2Solution: This is intentional. Explicit access:
- Eliminates ambiguity
- Makes code clearer and more maintainable
- Aligns with Go's philosophy of explicitness
- Users can choose to work with tuple values directly without extracting
Issue 5: Function-Style vs Struct Literal
Two construction syntaxes may cause confusion.
Solution: Clear guidelines:
- Function-style
(): Use for concise inline construction- Positional:
Point(3, 4)when order is obvious (all fields required) - Kwargs:
Point(x = 3, y = 4)when names matter (supports partial init)
- Positional:
- Struct literal
{}: Use for traditional Go style- Allows partial initialization with both positional and named syntax
- Supports
:syntax for named fields - More familiar to Go developers
Specification Summary
Key Design Decisions
-
Naming Scope: Field names only valid at compile time; runtime uses ordinal fields
-
Numeric Index Syntax: For anonymous tuples,
v.0is syntactic sugar forv.X_0 -
No Auto-Unpacking: Tuples must be accessed via explicit field access to avoid ambiguity
-
Type Shorthand: Consecutive same-type fields can be abbreviated:
(x, y int) -
Single-Element Equivalence:
(T)and(name T)both degenerate toT -
Construction Styles:
- Struct literal:
T{...}with:syntax (allows partial init) - Function-style positional:
T(v1, v2, ...)(all fields required) - Function-style kwargs:
T(f1 = v1, f2 = v2, ...)(supports partial init, any order) - No mixing of positional and kwargs in function-style
- Struct literal:
-
Structural Equivalence: Same structure = convertible, regardless of names
-
Reflection: Only sees ordinal fields
-
Reserved Fields:
X_0,X_1, ... are reserved
Construction Syntax Summary
type Point (x, y int)
// Valid construction methods:
// Struct literal style
Point{3, 4} // Positional (all fields)
Point{x: 3, y: 4} // Named with : (any order)
Point{x: 3} // Partial init (y = 0)
// Function-style
Point(3, 4) // Positional (all fields required)
Point(y = 4, x = 3) // Kwargs with = (any order, supports partial init)
Point(x = 3) // Kwargs partial init (y = 0)
// Invalid
Point(x: 3, y: 4) // ✗ Cannot use : in function-style
Point(3, y = 4) // ✗ Cannot mix positional and kwargs
Point(3) // ✗ Positional args don't support partial initThrough explicit field access design and flexible construction syntax, XGo's tuples provide a clean, unambiguous syntax that fits naturally with Go's philosophy while adding powerful multi-value composition capabilities.
- Proposal Draft: https://github.com/goplus/xgo/wiki/XGo-Tuple-Type-Proposal
- Tuple Types Proposal for Go: proposal: Tuple Types for Go golang/go#64457