Skip to content

Commit 02416eb

Browse files
committed
First draft of lazy operations
1 parent 086bca2 commit 02416eb

2 files changed

Lines changed: 274 additions & 0 deletions

File tree

include/vector.h

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
#pragma once
2424
#include <algorithm>
2525
#include <cassert>
26+
#include <functional>
2627
#include <type_traits>
2728
#include <vector>
2829
#include <iterator>
30+
#include <memory>
31+
#include <utility>
2932
#include "index_range.h"
3033
#include "optional.h"
3134
#ifdef PARALLEL_ALGORITHM_AVAILABLE
@@ -36,6 +39,193 @@ namespace fcpp {
3639
template <class T, class Compare>
3740
class set;
3841

42+
template <typename T>
43+
class vector;
44+
45+
// A lightweight wrapper representing a deferred vector pipeline, enabling fluent and functional
46+
// programming while avoiding intermediate vector materialization.
47+
//
48+
// Member functions are non-mutating and keep extending the pipeline. Terminal functions such as
49+
// `get` and `reduce` execute the stored operations.
50+
template <typename T>
51+
class lazy_vector
52+
{
53+
public:
54+
lazy_vector()
55+
: m_operation([](const std::function<void(const T&)>&) {})
56+
, m_capacity_hint(0)
57+
{
58+
}
59+
60+
// Creates a lazy vector by copying the provided std::vector as an owned source.
61+
explicit lazy_vector(const std::vector<T>& vector)
62+
: m_capacity_hint(vector.size())
63+
{
64+
auto source = std::make_shared<std::vector<T>>(vector);
65+
m_operation = [source](const std::function<void(const T&)>& consumer) {
66+
std::for_each(source->begin(), source->end(), consumer);
67+
};
68+
}
69+
70+
// Creates a lazy vector by moving the provided std::vector as an owned source.
71+
explicit lazy_vector(std::vector<T>&& vector)
72+
: m_capacity_hint(vector.size())
73+
{
74+
auto source = std::make_shared<std::vector<T>>(std::move(vector));
75+
m_operation = [source](const std::function<void(const T&)>& consumer) {
76+
std::for_each(source->begin(), source->end(), consumer);
77+
};
78+
}
79+
80+
// Creates a lazy vector by referring to an existing std::vector source.
81+
// The referenced vector must outlive this lazy vector.
82+
explicit lazy_vector(const std::vector<T>* vector)
83+
: m_capacity_hint(vector->size())
84+
{
85+
m_operation = [vector](const std::function<void(const T&)>& consumer) {
86+
std::for_each(vector->begin(), vector->end(), consumer);
87+
};
88+
}
89+
90+
// Creates a lazy vector by directly providing the deferred operation.
91+
// This constructor is mostly useful for composing lazy_vector instances.
92+
lazy_vector(std::function<void(const std::function<void(const T&)>&)> operation, size_t capacity_hint)
93+
: m_operation(std::move(operation))
94+
, m_capacity_hint(capacity_hint)
95+
{
96+
}
97+
98+
// Performs the functional `map` algorithm lazily. The transform is not applied until
99+
// a terminal operation, such as `get` or `reduce`, is called.
100+
//
101+
// example:
102+
// const fcpp::vector<int> input_vector({ 1, 3, -5 });
103+
// const auto output_vector = input_vector
104+
// .lazy()
105+
// .map<std::string>([](const auto& element) {
106+
// return std::to_string(element);
107+
// })
108+
// .get();
109+
//
110+
// outcome:
111+
// output_vector -> fcpp::vector<std::string>({ "1", "3", "-5" })
112+
#ifdef CPP17_AVAILABLE
113+
template <typename U, typename Transform, typename = std::enable_if_t<std::is_invocable_r_v<U, Transform, T>>>
114+
#else
115+
template <typename U, typename Transform>
116+
#endif
117+
[[nodiscard]] lazy_vector<U> map(Transform&& transform) const
118+
{
119+
const auto previous = m_operation;
120+
const auto capacity_hint = m_capacity_hint;
121+
typename std::decay<Transform>::type transform_copy(std::forward<Transform>(transform));
122+
return lazy_vector<U>(
123+
[previous, transform_copy](const std::function<void(const U&)>& consumer) mutable {
124+
previous([&consumer, &transform_copy](const T& element) {
125+
consumer(transform_copy(element));
126+
});
127+
},
128+
capacity_hint);
129+
}
130+
131+
// Performs the functional `map` algorithm lazily.
132+
// See also `map` for more documentation.
133+
#ifdef CPP17_AVAILABLE
134+
template <typename U, typename Transform, typename = std::enable_if_t<std::is_invocable_r_v<U, Transform, T>>>
135+
#else
136+
template <typename U, typename Transform>
137+
#endif
138+
[[nodiscard]] lazy_vector<U> mapped(Transform&& transform) const
139+
{
140+
return map<U>(std::forward<Transform>(transform));
141+
}
142+
143+
// Performs the functional `filter` algorithm lazily, in which all elements which match
144+
// the given predicate are kept. The predicate is not applied until a terminal operation,
145+
// such as `get` or `reduce`, is called.
146+
//
147+
// example:
148+
// const fcpp::vector<int> numbers({ 1, 3, -5, 2, -1, 9, -4 });
149+
// const auto filtered_numbers = numbers
150+
// .lazy()
151+
// .filter([](const auto& element) {
152+
// return element >= 1.5;
153+
// })
154+
// .get();
155+
//
156+
// outcome:
157+
// filtered_numbers -> fcpp::vector<int>({ 3, 2, 9 })
158+
#ifdef CPP17_AVAILABLE
159+
template <typename Filter, typename = std::enable_if_t<std::is_invocable_r_v<bool, Filter, T>>>
160+
#else
161+
template <typename Filter>
162+
#endif
163+
[[nodiscard]] lazy_vector filter(Filter&& predicate_to_keep) const
164+
{
165+
const auto previous = m_operation;
166+
const auto capacity_hint = m_capacity_hint;
167+
typename std::decay<Filter>::type predicate_copy(std::forward<Filter>(predicate_to_keep));
168+
return lazy_vector(
169+
[previous, predicate_copy](const std::function<void(const T&)>& consumer) mutable {
170+
previous([&consumer, &predicate_copy](const T& element) {
171+
if (predicate_copy(element)) {
172+
consumer(element);
173+
}
174+
});
175+
},
176+
capacity_hint);
177+
}
178+
179+
// Performs the functional `filter` algorithm lazily.
180+
// See also `filter` for more documentation.
181+
#ifdef CPP17_AVAILABLE
182+
template <typename Filter, typename = std::enable_if_t<std::is_invocable_r_v<bool, Filter, T>>>
183+
#else
184+
template <typename Filter>
185+
#endif
186+
[[nodiscard]] lazy_vector filtered(Filter&& predicate_to_keep) const
187+
{
188+
return filter(std::forward<Filter>(predicate_to_keep));
189+
}
190+
191+
// Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of
192+
// accumulating all the values in this lazy vector to an initial value.
193+
//
194+
// example:
195+
// const fcpp::vector<int> numbers({ 1, 3, -5, 2, -1, 9, -4 });
196+
// const auto sum = numbers
197+
// .lazy()
198+
// .filter([](const auto& element) {
199+
// return element > 0;
200+
// })
201+
// .reduce(0, [](const int& partial_sum, const int& number) {
202+
// return partial_sum + number;
203+
// });
204+
//
205+
// outcome:
206+
// sum -> 15
207+
#ifdef CPP17_AVAILABLE
208+
template <typename U, typename Reduce, typename = std::enable_if_t<std::is_invocable_r_v<U, Reduce, U, T>>>
209+
#else
210+
template <typename U, typename Reduce>
211+
#endif
212+
U reduce(const U& initial, Reduce&& reduction) const
213+
{
214+
auto result = initial;
215+
m_operation([&result, &reduction](const T& element) {
216+
result = reduction(result, element);
217+
});
218+
return result;
219+
}
220+
221+
// Materializes this lazy vector to a functional vector, executing all stored operations.
222+
[[nodiscard]] vector<T> get() const;
223+
224+
private:
225+
std::function<void(const std::function<void(const T&)>&)> m_operation;
226+
size_t m_capacity_hint;
227+
};
228+
39229
// A lightweight wrapper around std::vector, enabling fluent and functional
40230
// programming on the vector itself, rather than using the more procedural style
41231
// of the standard library algorithms.
@@ -1429,6 +1619,13 @@ namespace fcpp {
14291619
return *this;
14301620
}
14311621

1622+
// Starts a lazy pipeline. The returned lazy vector defers following map/filter
1623+
// transformations until a terminal operation, such as get() or reduce(), is called.
1624+
[[nodiscard]] lazy_vector<T> lazy() const
1625+
{
1626+
return lazy_vector<T>(&m_vector);
1627+
}
1628+
14321629
// Returns the begin iterator, useful for other standard library algorithms
14331630
[[nodiscard]] typename std::vector<T>::iterator begin()
14341631
{
@@ -1685,4 +1882,15 @@ namespace fcpp {
16851882
assert(index <= size());
16861883
}
16871884
};
1885+
1886+
template <typename T>
1887+
[[nodiscard]] vector<T> lazy_vector<T>::get() const
1888+
{
1889+
std::vector<T> materialized;
1890+
materialized.reserve(m_capacity_hint);
1891+
m_operation([&materialized](const T& element) {
1892+
materialized.push_back(element);
1893+
});
1894+
return vector<T>(std::move(materialized));
1895+
}
16881896
}

tests/vector_test.cc

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,4 +1375,70 @@ TEST(VectorTest, DistinctCustomType)
13751375
EXPECT_EQ(expected, unique_persons);
13761376
}
13771377

1378+
TEST(VectorTest, LazyMapFilterGet)
1379+
{
1380+
const vector<int> vector_under_test({1, 2, 3, 4});
1381+
int map_call_count = 0;
1382+
int filter_call_count = 0;
1383+
1384+
const auto lazy_vector = vector_under_test
1385+
.lazy()
1386+
.map<int>([&map_call_count](const int& value) {
1387+
++map_call_count;
1388+
return value * 2;
1389+
})
1390+
.filter([&filter_call_count](const int& value) {
1391+
++filter_call_count;
1392+
return value > 4;
1393+
});
1394+
1395+
EXPECT_EQ(0, map_call_count);
1396+
EXPECT_EQ(0, filter_call_count);
1397+
1398+
const auto materialized_vector = lazy_vector.get();
1399+
EXPECT_EQ(vector<int>({6, 8}), materialized_vector);
1400+
EXPECT_EQ(vector<int>({1, 2, 3, 4}), vector_under_test);
1401+
EXPECT_EQ(4, map_call_count);
1402+
EXPECT_EQ(4, filter_call_count);
1403+
}
1404+
1405+
TEST(VectorTest, LazyFiltered)
1406+
{
1407+
const vector<int> vector_under_test({1, 2, 3, 4});
1408+
const auto filtered_vector = vector_under_test
1409+
.lazy()
1410+
.filtered([](const int& value) {
1411+
return value % 2 == 0;
1412+
})
1413+
.get();
1414+
1415+
EXPECT_EQ(vector<int>({2, 4}), filtered_vector);
1416+
EXPECT_EQ(vector<int>({1, 2, 3, 4}), vector_under_test);
1417+
}
1418+
1419+
TEST(VectorTest, LazyReduce)
1420+
{
1421+
const vector<int> vector_under_test({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
1422+
int map_call_count = 0;
1423+
int filter_call_count = 0;
1424+
1425+
const auto result = vector_under_test
1426+
.lazy()
1427+
.map<int>([&map_call_count](const int& value) {
1428+
++map_call_count;
1429+
return value * 3;
1430+
})
1431+
.filter([&filter_call_count](const int& value) {
1432+
++filter_call_count;
1433+
return value > 5;
1434+
})
1435+
.reduce(0, [](const int& partial_sum, const int& value) {
1436+
return partial_sum + value;
1437+
});
1438+
1439+
EXPECT_EQ(162, result);
1440+
EXPECT_EQ(10, map_call_count);
1441+
EXPECT_EQ(10, filter_call_count);
1442+
}
1443+
13781444
#pragma warning( pop )

0 commit comments

Comments
 (0)