File: named_tuple.md

package info (click to toggle)
reflect-cpp 0.18.0%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 12,524 kB
  • sloc: cpp: 44,484; python: 131; makefile: 30; sh: 3
file content (267 lines) | stat: -rw-r--r-- 8,268 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# `rfl::NamedTuple`

`rfl::NamedTuple` is very similar to `std::tuple` or `rfl::Tuple`, but unlike 
these two structures, the fields have names.

In other words, consider the following struct:

```cpp
struct Person {
    std::string first_name;
    std::string last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;
};
```

You might as well define the following `rfl::NamedTuple`:

```cpp
using Person = rfl::NamedTuple<
    rfl::Field<"first_name", std::string>,
    rfl::Field<"last_name", std::string>,
    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>>;
```

From the point-of-view of serialization/deserialization, the two definitions are equivalent. 
The resulting JSON strings (or any other format) will be the same.


## Structural typing

From the point-of-view of programming, there is an important difference: Structs are *nominally typed*
and named tuples are *structurally typed* (confusingly, structs are not structurally typed).

In plain language, that means that the compiler will regard this as absolutely equivalent to `Person`, the named tuple:

```cpp
using Person2 = rfl::NamedTuple<
    rfl::Field<"first_name", std::string>,
    rfl::Field<"last_name", std::string>,
    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>>;
```

However, this will be seen as a type that is different from `Person`, the struct:

```cpp
struct Person2 {
    std::string first_name;
    std::string last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;
};
```

Structural typing also means that you can declare new types on-the-fly. For instance, in order
to create a `Person` named tuple, you don't actually have to declare it at all. The following will do:

```cpp
const auto person = rfl::Field<"first_name", std::string>("Homer") *
                    rfl::Field<"last_name", std::string>("Simpson") *
                    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>("1987-04-19");
```

The type of `person` will now be equivalent to the definition of `Person`, the named tuple, 
regardless of whether you have actually declared it anywhere.

On the other hand, structural typing also means that recursive definitions are impossible.
For instance, consider something like this:

```cpp
struct Person {
    std::string first_name;
    std::string last_name;
    std::vector<Person> children;
};
```

In this example, `Person` is recursively defined (because of the field `children`). 
This is impossible to accomplish using structural typing and `rfl::NamedTuple`, just like it is impossible to have a recursively defined lambda function.

## Accessing fields

Fields inside the named tuple can be accessed using `rfl::get` or the `.get` method:

```cpp
const auto person = rfl::Field<"first_name", std::string>("Homer") *
                    rfl::Field<"last_name", std::string>("Simpson") *
                    rfl::Field<"birthday", rfl::Timestamp<"%Y-%m-%d">>("1987-04-19");

// OK in most circumstances (there are restrictions
// due to the way C++ templates work).
const auto first_name = person.get<"firstName">();

// Always OK
const auto first_name = person.template get<"firstName">();

// Always OK
const auto first_name = rfl::get<"firstName">(person);
```

Fields can also be iterated over at compile-time using the `apply()` method:

```cpp
auto person = rfl::Field<"first_name", std::string>("Bart") *
              rfl::Field<"last_name", std::string>("Simpson");

person.apply([](const auto& f) {
  auto field_name = f.name();
  const auto& value = *f.value();
});

person.apply([]<typename Field>(Field& f) {
  // The field name can also be obtained as a compile-time constant.
  constexpr auto field_name = Field::name();
  using field_pointer_type = typename Field::Type;
  field_pointer_type* value = f.value();
});
```

### Monadic operations: `.transform` and `.and_then`

Named tuples also contain compile-time monadic operations. 

`.transform(f)` expects a function `f` of type `Field -> Field`. 
`transform` then applies that function to each field of the named tuple.
It can be used to change either the values or the names of the fields, but
not their overall number.

```cpp
const auto lisa =
    Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8};

const auto to_bart = [](auto field) {
if constexpr (decltype(field)::name() == "first_name") {
    field = "Bart";
    return field;
} else if constexpr (decltype(field)::name() == "age") {
    field = 10;
    return field;
} else {
    return field;
}
};

// bart will now be a named tuple with first_name="Bart",
// last_name="Simpson", age=10
const auto bart = rfl::to_named_tuple(lisa).transform(to_bart);
```

`.and_then(f)` expects a function `f` of type `Field -> NamedTuple`. 
`and_then` then applies that function to each field of the named tuple
and finally concatenates the resulting named tuple to form a new named tuple. 
Note that the named tuple returned by `f` may be empty. `.and_then(f)` can be used 
to change either the values or the names of the fields, and can also affect
their overall number.

```cpp
const auto lisa =
    Person{.first_name = "Lisa", .last_name = "Simpson", .age = 8};

const auto to_bart = [](auto field) {
if constexpr (decltype(field)::name() == "first_name") {
    field = "Bart";
    return rfl::make_named_tuple(field);
} else if constexpr (decltype(field)::name() == "age") {
    return rfl::make_named_tuple();
} else {
    return rfl::make_named_tuple(field);
}
};

// bart will now be a named tuple with first_name="Bart",
// last_name="Simpson". Since we have returned and empty
// named tuple for the field "age", there will be no such
// field in bart.
const auto bart = rfl::to_named_tuple(lisa).and_then(to_bart);
```

### `rfl::replace`

`rfl::replace` works for `rfl::NamedTuple` as well:

```cpp
const auto lisa = rfl::Field<"firstName", std::string>("Lisa") *
                  rfl::Field<"lastName", std::string>("Simpson");

// Returns a deep copy of the original object,
// replacing first_name.
const auto maggie =
    rfl::replace(lisa, rfl::make_field<"firstName">(std::string("Maggie")));

// Also OK
const auto bart = lisa.replace(rfl::make_field<"firstName">(std::string("Bart")));
```

### `rfl::as`

So does `rfl::as`:

```cpp
using C = rfl::NamedTuple<
    rfl::Field<"f1", std::string>,
    rfl::Field<"f2", std::string>,
    rfl::Field<"f4", std::string>>;

const auto a = rfl::Field<"f1", std::string>("Hello") *  
               rfl::Field<"f2", std::string>("World");

const auto b = rfl::Field<"f3", std::string>("Hello") *  
               rfl::Field<"f4", std::string>("World");

const auto c = rfl::as<C>(a, b);
```

However, you do not really have to use `rfl::as` here. This will work as well:

```cpp
// Same as rfl::as<C>(a, b)
const auto c = C(a * b);
```

(in fact, this is how `rfl::as` is implemented in the first place).

## Defining named tuples using other named tuples

`rfl::Flatten` is not supported inside named tuples. Instead, you can use `rfl::define_named_tuple_t<...>`
to achieve the same goal:

```cpp
using Person = rfl::NamedTuple<
    rfl::Field<"firstName", std::string>,
    rfl::Field<"lastName", std::string>,
    rfl::Field<"age", int>>;

using Employee = rfl::define_named_tuple_t<
    Person,
    rfl::Field<"salary", float>>;

const auto employee = Employee(
    rfl::Field<"firstName", std::string>("Homer"),
    rfl::Field<"lastName", std::string>("Simpson"),
    rfl::make_field<"age">(45),
    rfl::make_field<"salary">(60000.0));
```

## Transforming structs to named tuples and vice versa

You can transform structs to named tuples and vice versa (this will only work with the `rfl::Field`-syntax:

```cpp
auto bart = Person{.first_name = "Bart",
                   .last_name = "Simpson",
                   .birthday = "1987-04-19"};

// bart_nt is a named tuple
const auto bart_nt = rfl::to_named_tuple(bart);

// You can also retrieve the equivalent named tuple
// type to a struct:
using PersonNamedTuple = rfl::named_tuple_t<Person>;

// rfl::to_named_tuple also supports move semantics
PersonNamedTuple bart_nt = rfl::to_named_tuple(std::move(bart_nt));

// You can also go the other way
const auto bart_struct = rfl::from_named_tuple<Person>(bart_nt);
const auto bart_struct = rfl::from_named_tuple<Person>(std::move(bart_nt));
```