|
| 1 | +# Replace type with tuple struct |
| 2 | + |
| 3 | +* Proposal: [ME-0008](https://github.com/moonbitlang/moonbit-evolution/blob/0008-tuple-struct/proposals/0008-tuple-struct.mbt.md) |
| 4 | +* Author: [whitepie](https://github.com/whitepie) |
| 5 | +* Status: Draft |
| 6 | +* Review and discussion: [GitHub issue](https://github.com/moonbitlang/moonbit-evolution/issues/) |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +Currently, MoonBit uses the `type` syntax for implementing the newtype idiom, which creates a new type that wraps an existing type. However, the current syntax has several issues, particularly when dealing with tuple types. We propose to replace the `type` syntax with a more intuitive `tuple struct` syntax that provides clearer semantics and better ergonomics while maintaining the newtype pattern. |
| 11 | + |
| 12 | +## Motivation |
| 13 | + |
| 14 | +### Problems with current type syntax |
| 15 | + |
| 16 | +The current `type` syntax for implementing the newtype idiom with tuple types creates ambiguity and inconsistency: |
| 17 | + |
| 18 | +```moonbit |
| 19 | +type Newtype Int |
| 20 | +type NewtypeTuple (Int, Int) |
| 21 | +``` |
| 22 | + |
| 23 | +For tuple types, the semantics are unclear and there are multiple ways to construct and access elements: |
| 24 | + |
| 25 | +#### Construction ambiguity |
| 26 | +```moonbit |
| 27 | +fn make_newtype_tuple1(x: Int, y: Int) -> NewtypeTuple { |
| 28 | + (x, y) // Direct tuple conversion |
| 29 | +} |
| 30 | +
|
| 31 | +fn make_newtype_tuple2(x: Int, y: Int) -> NewtypeTuple { |
| 32 | + NewtypeTuple((x, y)) // Constructor with extra parentheses |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +#### Access ambiguity |
| 37 | +```moonbit |
| 38 | +fn get_x1(t: NewtypeTuple) -> Int { |
| 39 | + t.0 // Direct field access |
| 40 | +} |
| 41 | +
|
| 42 | +fn get_x2(t: NewtypeTuple) -> Int { |
| 43 | + t.inner().0 // Using .inner() method then field access |
| 44 | +} |
| 45 | +
|
| 46 | +fn get_x3(t: NewtypeTuple) -> Int { |
| 47 | + match t { |
| 48 | + NewtypeTuple((x, _)) => x // Pattern matching |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +These inconsistencies become even more problematic when dealing with value types and create cognitive overhead for developers. The newtype pattern should provide clear, unambiguous semantics. |
| 54 | + |
| 55 | +## Proposed solution |
| 56 | + |
| 57 | +We propose introducing `tuple struct` syntax as a replacement for the `type` syntax when implementing the newtype idiom: |
| 58 | + |
| 59 | +```moonbit |
| 60 | +struct Single(Int) |
| 61 | +struct Multiple(Int, String, Char) |
| 62 | +``` |
| 63 | + |
| 64 | +### Benefits of tuple struct syntax |
| 65 | + |
| 66 | +1. **Clearer semantics**: Tuple structs have unambiguous construction and access patterns |
| 67 | +2. **Consistent access**: Use `.0`, `.1`, etc. for all tuple structs |
| 68 | +3. **Simpler construction**: Constructor takes the same number of arguments as the struct definition |
| 69 | +4. **Better value type support**: Easier to implement and reason about |
| 70 | +5. **Maintains newtype pattern**: Still provides type safety and distinct semantics from the underlying type |
| 71 | + |
| 72 | +### Construction and access |
| 73 | + |
| 74 | +```moonbit |
| 75 | +struct Multiple(Int, String, Char) |
| 76 | +
|
| 77 | +fn make_multiple(a: Int, b: String, c: Char) -> Multiple { |
| 78 | + Multiple(a, b, c) // Clear constructor syntax |
| 79 | +} |
| 80 | +
|
| 81 | +fn use_multiple(x: Multiple) -> Unit { |
| 82 | + println(x.0) // Access first element |
| 83 | + println(x.1) // Access second element |
| 84 | + println(x.2) // Access third element |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +## Special cases and features |
| 89 | + |
| 90 | +### Single-element tuple structs |
| 91 | + |
| 92 | +Single-element tuple structs are guaranteed to be unboxed at runtime and provide several convenient shorthands, maintaining the efficiency benefits of the newtype pattern: |
| 93 | + |
| 94 | +```moonbit |
| 95 | +struct S { |
| 96 | + a: Int |
| 97 | +} |
| 98 | +
|
| 99 | +struct R(S) |
| 100 | +
|
| 101 | +fn get_a(r: R) -> Int { |
| 102 | + r.a // Automatic field forwarding |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +### Function type support |
| 107 | + |
| 108 | +When the element type is a function, tuple structs provide seamless function application: |
| 109 | + |
| 110 | +```moonbit |
| 111 | +struct F((Int) -> Int) |
| 112 | +
|
| 113 | +fn apply(f: F, x: Int) -> Int { |
| 114 | + f(x) // No need for .0 when applying |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## Migration strategy |
| 119 | + |
| 120 | +### Single-element types |
| 121 | + |
| 122 | +For single-element types where the underlying type is not a tuple, the formatter automatically migrates to the new syntax: |
| 123 | + |
| 124 | +```moonbit |
| 125 | +// Old syntax |
| 126 | +type A1 Int |
| 127 | +fn A1::get(a: A1) -> Int { |
| 128 | + a.inner() |
| 129 | +} |
| 130 | +
|
| 131 | +// New syntax (automatically migrated) |
| 132 | +struct A2(Int) |
| 133 | +fn A2::get(a: A2) -> Int { |
| 134 | + a.0 |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +To facilitate migration, single-element tuple structs temporarily provide a `.inner()` method, which will be deprecated and removed in future versions. |
| 139 | + |
| 140 | +### Multi-element types |
| 141 | + |
| 142 | +Multi-element tuple structs differ from the old tuple type syntax in important ways: |
| 143 | + |
| 144 | +1. **No direct tuple construction**: Tuple structs cannot be constructed directly from tuples |
| 145 | +2. **No .inner() method**: Tuple structs do not provide access to the underlying tuple |
| 146 | + |
| 147 | +### Compatibility with tuple conversion |
| 148 | + |
| 149 | +If you need a tuple struct that can be directly converted to and from tuples while maintaining the newtype pattern, you can wrap the tuple type: |
| 150 | + |
| 151 | +```moonbit |
| 152 | +struct T((Int, Int)) |
| 153 | +
|
| 154 | +fn make_t(x: Int, y: Int) -> T { |
| 155 | + (x, y) // Direct tuple construction |
| 156 | +} |
| 157 | +
|
| 158 | +fn use_t(t: T) -> (Int, Int) { |
| 159 | + t.0 // Access the wrapped tuple |
| 160 | +} |
| 161 | +
|
| 162 | +// Access individual elements requires nested access |
| 163 | +fn get_x(t: T) -> Int { |
| 164 | + t.0.0 // t.0 gets the tuple, .0 gets the first element |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +## Conclusion |
| 169 | + |
| 170 | +The tuple struct syntax provides a cleaner, more intuitive alternative to the current `type` syntax while maintaining the newtype idiom's benefits. It eliminates ambiguity in construction and access patterns while preserving type safety and distinct semantics from the underlying types. The migration strategy ensures backward compatibility while encouraging adoption of the new syntax. |
| 171 | + |
| 172 | +This proposal aligns with MoonBit's goal of providing a clear, consistent language design that reduces cognitive overhead for developers while maintaining the powerful newtype pattern for type safety and domain modeling. |
0 commit comments