Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Tuples

Tuples are for combining multiple values to be used as a single object. They are implemented as a library feature by the Tuple template from the std.typecons module.

Tuple makes use of AliasSeq from the std.meta module for some of its operations.

This chapter covers only the more common operations of tuples. For more information on tuples and templates see Philippe Sigaud's D Templates: A Tutorial.

Tuple and tuple()

Tuples are usually constructed by the convenience function tuple():

import std.stdio;
import std.typecons;

void main() {
    auto t = tuple(42, "hello");
    writeln(t);
}

The tuple call above constructs an object that consists of the int value 42 and the string value "hello". The output of the program includes the type of the tuple object and its members:

Tuple!(int, string)(42, "hello")

The tuple type above is the equivalent of the following pseudo struct definition and likely have been implemented in exactly the same way:

// The equivalent of Tuple!(int, string)
struct __Tuple_int_string {
    int __member_0;
    string __member_1;
}

The members of a tuple are normally accessed by their index values. That syntax suggests that tuples can be seen as arrays consisting of different types of elements:

    writeln(t[0]);
    writeln(t[1]);

The output:

42
hello
Member properties

It is possible to access members by properties if tuple and Tuple templates are instantiated with member names. Both of the methods below have the same effect:

    auto a = tuple!("number", "message")(42, "hello");
    auto b = Tuple!(int, "number", string, "message")(42, "hello");

The definitions above allow accessing the members by .number and .message properties as well:

    writeln("by index 0 : ", a[0]);
    writeln("by .number : ", a.number);
    writeln("by index 1 : ", a[1]);
    writeln("by .message: ", a.message);

The output:

by index 0 : 42
by .number : 42
by index 1 : hello
by .message: hello
Expanding the members as a list of values

Tuple members can be expanded as a list of values that can be used e.g. as an argument list when calling a function. The members can be expanded either by the .expand property or by slicing:

import std.stdio;
import std.typecons;

void foo(int i, string s, double d, char c) {
    // ...
}

void bar(int i, double d, char c) {
    // ...
}

void main() {
    auto t = tuple(1, "2", 3.3, '4');

    // Both of the following lines are equivalents of
    // foo(1, "2", 3.3, '4'):
    foo(t.expand);
    foo(t[]);

    // The equivalent of bar(1, 3.3, '4'):
    bar(t[0], t[$-2..$]);
}

The tuple above consists of four values of int, string, double, and char. Since those types match the parameter list of foo(), an expansion of its members can be used as arguments to foo(). When calling bar(), a matching argument list is made up of the first member and the last two members of the tuple.

As long as the members are compatible to be elements of the same array, the expansion of a tuple can be used as the element values of an array literal as well:

import std.stdio;
import std.typecons;

void main() {
    auto t = tuple(1, 2, 3);
    auto a = [ t.expand, t[] ];
    writeln(a);
}

The array literal above is initialized by expanding the same tuple twice:

[1, 2, 3, 1, 2, 3]
Compile-time foreach

Because their values can be expanded, tuples can be used with the foreach statement as well:

    auto t = tuple(42, "hello", 1.5);

    foreach (i, member; t) {
        writefln("%s: %s", i, member);
    }

The output:

0: 42
1: hello
2: 1.5

The foreach statement above may give a false impression: It may be thought of being a loop that gets executed at run time. That is not the case. Rather, a foreach statement that operates on the members of a tuple is an unrolling of the loop body for each member. The foreach statement above is the equivalent of the following code:

    {
        enum size_t i = 0;
        int member = t[i];
        writefln("%s: %s", i, member);
    }
    {
        enum size_t i = 1;
        string member = t[i];
        writefln("%s: %s", i, member);
    }
    {
        enum size_t i = 2;
        double member = t[i];
        writefln("%s: %s", i, member);
    }

The reason for the unrolling is the fact that when the tuple members are of different types, the foreach body has to be compiled differently for each type.

We will see static foreach, a more powerful loop unrolling feature, in a later chapter.

Returning multiple values from functions

Tuples can be a simple solution to the limitation of functions having to return a single value. An example of this is std.algorithm.findSplit. findSplit() searches for a range inside another range and produces a result consisting of three pieces: the part before the found range, the found range, and the part after the found range:

import std.algorithm;

// ...

    auto entireRange = "hello";
    auto searched = "ll";

    auto result = findSplit(entireRange, searched);

    writeln("before: ", result[0]);
    writeln("found : ", result[1]);
    writeln("after : ", result[2]);

The output:

before: he
found : ll
after : o

Another option for returning multiple values from a function is to return a struct object:

struct Result {
    // ...
}

Result foo() {
    // ...
}
AliasSeq

AliasSeq is defined in the std.meta module. It is used for representing a concept that is normally used by the compiler but otherwise not available to the programmer as an entity: A comma-separated list of values, types, and symbols (i.e. alias template arguments). The following are three examples of such lists:

The following three lines of code are examples of those lists in the same order:

    foo(1, "hello", 2.5);         // function arguments
    auto o = Bar!(char, long)();  // template arguments
    auto a = [ 1, 2, 3, 4 ];      // array literal elements

Tuple takes advantage of AliasSeq when expanding its members.

The name AliasSeq comes from "alias sequence" and it can contain types, values, and symbols. (AliasSeq and std.meta used to be called TypeTuple and std.typetuple, respectively.)

This chapter includes AliasSeq examples that consist only of types or only of values. Examples of its use with both types and values will appear in the next chapter. AliasSeq is especially useful with variadic templates, which we will see in the next chapter as well.

AliasSeq consisting of values

The values that an AliasSeq represents are specified as its template arguments.

Let's imagine a function that takes three parameters:

import std.stdio;

void foo(int i, string s, double d) {
    writefln("foo is called with %s, %s, and %s.", i, s, d);
}

That function would normally be called with three arguments:

    foo(1, "hello", 2.5);

AliasSeq can combine those arguments as a single entity and can automatically be expanded when calling functions:

import std.meta;

// ...

    alias arguments = AliasSeq!(1, "hello", 2.5);
    foo(arguments);

Although it looks like the function is now being called with a single argument, the foo() call above is the equivalent of the previous one. As a result, both calls produce the same output:

foo is called with 1, hello, and 2.5.

Also note that arguments is not defined as a variable, e.g. with auto. Rather, it is an alias of a specific AliasSeq instance. Although it is possible to define variables of AliasSeqs as well, the examples in this chapter will use them only as aliases.

As we have seen above with Tuple, when the values are compatible to be elements of the same array, an AliasSeq can be used to initialize an array literal as well:

    alias elements = AliasSeq!(1, 2, 3, 4);
    auto arr = [ elements ];
    assert(arr == [ 1, 2, 3, 4 ]);
Indexing and slicing

Same with Tuple, the members of an AliasSeq can be accessed by indexes and slices:

    alias arguments = AliasSeq!(1, "hello", 2.5);
    assert(arguments[0] == 1);
    assert(arguments[1] == "hello");
    assert(arguments[2] == 2.5);

Let's assume there is a function with parameters matching the last two members of the AliasSeq above. That function can be called with a slice of just the last two members of the AliasSeq:

void bar(string s, double d) {
    // ...
}

// ...

    bar(arguments[$-2 .. $]);
AliasSeq consisting of types

Members of an AliasSeq can consist of types. In other words, not a specific value of a specific type but a type like int itself. An AliasSeq consisting of types can represent template arguments.

Let's use an AliasSeq with a struct template that has two parameters. The first parameter of this template determines the element type of a member array and the second parameter determines the return value of a member function:

import std.conv;

struct S(ElementT, ResultT) {
    ElementT[] arr;

    ResultT length() {
        return to!ResultT(arr.length);
    }
}

void main() {
    auto s = S!(double, int)([ 1, 2, 3 ]);
    auto l = s.length();
}

In the code above, we see that the template is instantiated with (double, int). An AliasSeq can represent the same argument list as well:

import std.meta;

// ...

    alias Types = AliasSeq!(double, int);
    auto s = S!Types([ 1, 2, 3 ]);

Although it appears to be a single template argument, Types gets expanded automatically and the template instantiation becomes S!(double, int) as before.

AliasSeq is especially useful in variadic templates. We will see examples of this in the next chapter.

foreach with AliasSeq

Same with Tuple, the foreach statement operating on an AliasSeq is not a run time loop. Rather, it is the unrolling of the loop body for each member.

Let's see an example of this with a unit test written for the S struct that was defined above. The following code tests S for element types int, long, and float (ResultT is always size_t in this example):

unittest {
    alias Types = AliasSeq!(int, long, float);

    foreach (Type; Types) {
        auto s = S!(Type, size_t)([ Type.init, Type.init ]);
        assert(s.length() == 2);
    }
}

The foreach variable Type corresponds to int, long, and float, in that order. As a result, the foreach statement gets compiled as the equivalent of the code below:

    {
        auto s = S!(int, size_t)([ int.init, int.init ]);
        assert(s.length() == 2);
    }
    {
        auto s = S!(long, size_t)([ long.init, long.init ]);
        assert(s.length() == 2);
    }
    {
        auto s = S!(float, size_t)([ float.init, float.init ]);
        assert(s.length() == 2);
    }
.tupleof property

.tupleof represents the members of a type or an object. When applied to a user-defined type, .tupleof provides access to the definitions of the members of that type:

import std.stdio;

struct S {
    int number;
    string message;
    double value;
}

void main() {
    foreach (i, MemberType; typeof(S.tupleof)) {
        writefln("Member %s:", i);
        writefln("  type: %s", MemberType.stringof);

        string name = S.tupleof[i].stringof;
        writefln("  name: %s", name);
    }
}

S.tupleof appears in two places in the program. First, the types of the elements are obtained by applying typeof to .tupleof so that each type appears as the MemberType variable. Second, the name of the member is obtained by S.tupleof[i].stringof.

Member 0:
  type: int
  name: number
Member 1:
  type: string
  name: message
Member 2:
  type: double
  name: value

.tupleof can be applied to an object as well. In that case, it produces a tuple consisting of the values of the members of the object:

    auto object = S(42, "hello", 1.5);

    foreach (i, member; object.tupleof) {
        writefln("Member %s:", i);
        writefln("  type : %s", typeof(member).stringof);
        writefln("  value: %s", member);
    }

The foreach variable member represents each member of the object:

Member 0:
  type : int
  value: 42
Member 1:
  type : string
  value: hello
Member 2:
  type : double
  value: 1.5

Here, an important point to make is that the tuple that .tupleof returns consists of the members of the object themselves, not their copies. In other words, the tuple members are references to the actual object members.

Summary