Skip to content

XGo Tuple Type Proposal

xushiwei edited this page Jan 24, 2026 · 9 revisions

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:

  1. Transmitting multiple values through channels: Currently requires defining structs or using multiple channels
  2. Storing multiple values in maps: Requires explicit struct type definitions
  3. 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 to 100
    • (T) and (name T) both degenerate to T
  • 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.xv.X_0
v.yv.X_1

Type 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

  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
  2. 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
  3. 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
  4. 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)  // OK

Type 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 structure

Single-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 runtime

Type 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 works

Function-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 error

Important: Function-style construction only supports:

  1. Positional arguments: Point(3, 4)
  2. 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 equivalent

Keyword 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:

  1. Either all positional OR all kwargs - no mixing allowed
  2. Kwargs use = for assignment
  3. Kwargs can appear in any order
  4. Cannot specify the same field twice
  5. 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 init

Struct 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 error
As 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 = 42

Rationale

Why add function-style syntax?

  1. Consistency: Tuples are value types; constructing them looks like calling a function
  2. Conciseness: Point(3, 4) is more compact than Point{3, 4}
  3. Familiarity: Similar to constructor syntax in other languages (Python's Point(x=3, y=4), C++, Rust)
  4. Readability: More natural when passing tuples as arguments or in nested expressions
  5. Flexibility: kwargs with = syntax allows specifying fields in any order, improving code clarity for tuples with many fields
  6. 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?

  1. Simplicity: Two clear modes are easier to understand than complex mixing rules
  2. Consistency: Aligns with function call semantics in many languages
  3. Clarity: Forces explicit choice between positional (when order is obvious) or kwargs (when names matter)
  4. No ambiguity: Prevents confusion about which fields are being set

Compiler Implementation

The compiler handles function-style construction as follows:

  1. Syntax Recognition: Distinguish T(args...) from T{args...}
  2. Argument Mode Detection:
    • Check if all arguments are positional
    • Check if all arguments use = syntax (kwargs)
    • Reject mixed modes
  3. Transformation:
    • Positional: T(v1, v2, ...)T{v1, v2, ...}
    • Kwargs: T(f1 = v1, f2 = v2, ...)T{f1: v1, f2: v2, ...}
  4. Validation: Apply standard struct literal validation rules
  5. 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 access

Problem: 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:

  1. Receive the complete tuple value
  2. 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.2

Example 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: 0

Compiler Implementation Key Points

  1. Name Resolution Phase:

    • Record mapping from named tuple field names to ordinals
    • Record mapping from numeric indices to ordinals (v.0v.X_0)
    • Verify names don't conflict with reserved ordinals
    • Handle kwargs (= syntax) vs struct literal (: syntax) distinction
  2. 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...) to T{args...}
    • Validate kwargs ordering and compatibility
  3. 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
  4. 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

  1. No Ambiguity: Explicit field access eliminates comma-ok ambiguity
  2. Type Safety: Compile-time type checking
  3. Zero-Cost Abstraction: Compiles to structs with no runtime overhead
  4. Code Clarity: Named fields and numeric indices both available
  5. Progressive Enhancement: Optional feature, doesn't break existing code
  6. Simple Semantics: Tuples are just structs with convenient syntax
  7. Flexible Construction: Multiple construction styles (positional, named, kwargs)
  8. 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.2

Solution: 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)
  • 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

  1. Naming Scope: Field names only valid at compile time; runtime uses ordinal fields

  2. Numeric Index Syntax: For anonymous tuples, v.0 is syntactic sugar for v.X_0

  3. No Auto-Unpacking: Tuples must be accessed via explicit field access to avoid ambiguity

  4. Type Shorthand: Consecutive same-type fields can be abbreviated: (x, y int)

  5. Single-Element Equivalence: (T) and (name T) both degenerate to T

  6. 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
  7. Structural Equivalence: Same structure = convertible, regardless of names

  8. Reflection: Only sees ordinal fields

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

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


Clone this wiki locally