-
Notifications
You must be signed in to change notification settings - Fork 563
XGo Tuple Type Proposal
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.
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.
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)
-
Critical Specification: Named fields in tuples (such as name0 T0) are compile-time aliases that are uniformly converted to ordinal fields after compilation.
// 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_1Following 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)// 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)-
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.
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"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) // OKTwo 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 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 runtime// 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)// 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 worksTuple types can be viewed as constructor functions. For a named tuple type, function-style calls provide a concise way to construct tuple values.
// 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.
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 equivalentKwargs 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)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 errortype 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))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
}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,
)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 = 42Why 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
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.
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)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))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.
Important Design Decision: Tuples do NOT support automatic unpacking. All tuple elements must be accessed explicitly via field access.
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)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 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)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.
// 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)
}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
}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.2// 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)// Compiler infers tuple element types from context
m := map[string](int, string){
"a": (1, "one"),
"b": (2, "two"),
}// 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)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: 0-
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
- Function multi-return syntax unchanged
- Existing code requires no modifications
- Tuples are optional syntactic sugar
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)
}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{}- 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
x := (42) // Is this a tuple or expression?Solution: Single-element tuples degenerate to base type, so (42) is just 42.
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)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
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
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
-
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
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: https://github.com/golang/go/issues/64457