A tuple is a data structure that groups multiple data elements in a lightweight data structure. Tuples can be used as an easy way to represent multiple data elements, pass them as argument(s) or receive them as a return value, without the need to declare a complete new class or structure for holding the data set.
For example, the following code uses the generic System.Collections.Generic.Tuple type to receive multiple values from a function, without the need to declare multiple REF/OUT parameters or to create a new dedicated type for holding them:
USING System.Collections.Generic
FUNCTION GetCustomerData() AS Tuple<STRING, INT, LOGIC>
LOCAL oCustomer AS Tuple<STRING, INT, LOGIC>
oCustomer := Tuple<STRING, INT, LOGIC>{"Nikos", 47, TRUE}
RETURN oCustomer
FUNCTION Start() AS VOID
LOCAL oCustomer AS Tuple<STRING, INT, LOGIC>
oCustomer := GetCustomerData()
? "Customer name:", oCustomer:Item1
? "Age:", oCustomer:Item2
? "Is active:", oCustomer:Item3
X# also supports the dedicated keyword TUPLE, which offers an easier syntax for declaring and working with tuples. The above code can be written in a simpler way with the TUPLE keyword syntax:
FUNCTION GetCustomerData() AS TUPLE(STRING, INT, LOGIC)
LOCAL oCustomer AS TUPLE(STRING, INT, LOGIC) // defining the tuple
// tuple item types must match the types in the Tuple definition
oCustomer := TUPLE{"Nikos", 47, TRUE}
RETURN oCustomer
FUNCTION Start() AS VOID
LOCAL oCustomer AS (STRING, INT, LOGIC)
// the TUPLE keyword in the tuple definition can even be omitted!
oCustomer := GetCustomerData()
? "Customer name:", oCustomer:Item1
? "Age:", oCustomer:Item2
? "Is active:", oCustomer:Item3
Note that when using the TUPLE keyword, the compiler internally uses the type System.ValueTuple, instead of the generic type System.Collections.Generic.Tuple. Also when defining the tuple, the TUPLE keyword can be omitted, thus using the more c#-like syntax (type1, type2, ...)
The dedicated TUPLE keyword syntax supports also specifying custom names for the tuple items, instead of using the generic names Item1, Item2 etc. So the code can become much more readable and close to what it would be like if using a separate new class for the data:
FUNCTION Start() AS VOID
LOCAL oCustomer AS TUPLE(Name AS STRING, Age AS INT, IsActive AS LOGIC)
// provide names for each item of the tuple
oCustomer := GetCustomerData()
? "Customer name:", oCustomer:Name
? "Age:", oCustomer:Age
? "Is active:", oCustomer:IsActive
to make code even more self-explanatory, a named tuple can also be instantiated by using named arguments:
FUNCTION GetCustomerData() AS Tuple(STRING, INT, LOGIC)
// defining the named tuple with custom item names
LOCAL oCustomer AS Tuple(Name AS STRING, Age AS INT, IsActive AS LOGIC)
// including tuple item names in the tuple instantiation
oCustomer := TUPLE{Name := "Nikos", Age := 47, IsActive := TRUE}
RETURN oCustomer
Tuples can also be defined and instantiated at the same time using the VAR keyword:
FUNCTION Start() AS VOID
VAR oTuple := TUPLE{"Nikos", 47, FALSE}
? oTuple:Item1, oTuple:Item2, oTuple:Item3 // "Nikos", 47, FALSE
In this case, the data type of each tuple item is inferred from the item types supplied in the code. In the above sample, the item types are (STRING, INT, LOGIC).
Tuples with the same item types can be assigned to each other (thus copying the item values from the source tuple to the destination tuple), and the same can be done for tuples defined with either LOCAL or VAR:
FUNCTION Start() AS VOID
VAR oTuple := TUPLE{"Nikos", 47, FALSE}
LOCAL oNew AS TUPLE(STRING, INT, LOGIC)
oNew := oTuple
? oNew:Item1, oNew:Item2, oNew:Item3 // "Nikos", 47, FALSE
When defining a tuple with the VAR keyword, item names can be provided as well:
FUNCTION Start() AS VOID
VAR oTuple := TUPLE{Name := "Nikos", Age := 47, IsActive := FALSE}
? oTuple:Name, oTuple:Age, oTuple:IsActive // "Nikos", 47, FALSE
Finally, when defining a tuple with VAR by using identifiers for the item values, instead of using literal values, then each item automatically gets a name by the identifier name used for it:
CLASS CustomerInfo
EXPORT Description := "Customer description" AS STRING
END CLASS
FUNCTION Start() AS VOID
LOCAL name := "Unknown Customer" AS STRING
LOCAL oInfo := CustomerInfo{} AS CustomerInfo
VAR oTuple := TUPLE{name, oInfo:Description}
? oTuple:name // "Unknown Customer"
? oTuple:description // "Customer description"
A tuple can be deconstructed to multiple plain variables in one line of code, using the (var1, var2, ...) syntax:
FUNCTION Start() AS VOID
LOCAL oCustomer AS TUPLE(Name AS STRING, Age AS INT)
oCustomer := TUPLE{"Nikos", 47}
LOCAL name AS STRING
LOCAL age AS INT
(name, age) := oCustomer
? name, age // "Nikos", 47
The local variables can also be defined and assigned to the tuple item values in a single line, with a special LOCAL syntax for tuples:
FUNCTION Start() AS VOID
LOCAL oCustomer AS TUPLE(Name AS STRING, Age AS INT)
oCustomer := TUPLE{"Nikos", 47}
LOCAL (name AS STRING, age AS INT) := oCustomer
? name, age // "Nikos", 47
// You can also deconstruct into existing local variables without the LOCAL keyword:
(name, age) := oCustomer
Also the VAR keyword can be used for the tuple deconstruction, in which case the variable types are inferred from the tuple item types:
FUNCTION Start() AS VOID
LOCAL oCustomer AS TUPLE(Name AS STRING, Age AS INT)
oCustomer := TUPLE{"Nikos", 47}
VAR (name, age) := oCustomer
? name, age // "Nikos", 47