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));
```
|