Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement filter_map #192

Merged
merged 2 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/html)
set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html)

file(GLOB_RECURSE SPHINX_RST_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.rst)

add_custom_command(
OUTPUT ${SPHINX_INDEX_FILE}
COMMAND
${SPHINX_EXECUTABLE} -b html ${SPHINX_SOURCE} ${SPHINX_BUILD}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/index.rst
${SPHINX_RST_FILES}
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/conf.py
COMMENT "Generating documentation with Sphinx"
)
Expand Down
87 changes: 87 additions & 0 deletions docs/reference/adaptors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,93 @@ You can pass a reference to a sequence into an adaptor using :func:`flux::ref` o
* - :concept:`const_iterable_sequence`
- :var:`Seq` is const-iterable and :var:`Pred` is const-invocable

``filter_deref``
^^^^^^^^^^^^^^^^

.. function::
template <sequence Seq> \
requires optional_like<element_t<Seq>> \
auto filter_deref(Seq seq) -> sequence auto;

Given a sequence of "optional-like" elements (e.g. :type:`std::optional`, :type:`flux::optional`, :expr:`T*` etc...), filters out those elements which return :texpr:`false` after conversion to :texpr:`bool`, and performs a dereference of the remaining elements.

Equivalent to :expr:`filter_map(seq, std::identity{})`.

:models:

.. list-table::
:align: left
:header-rows: 1

* - Concept
- When
* - :concept:`multipass_sequence`
- :var:`Seq` is multipass
* - :concept:`bidirectional_sequence`
- :var:`Seq` is bidirectional
* - :concept:`random_access_sequence`
- Never
* - :concept:`contiguous_sequence`
- Never
* - :concept:`bounded_sequence`
- :var:`Seq` is bounded
* - :concept:`sized_sequence`
- Never
* - :concept:`infinite_sequence`
- :var:`Seq` is infinite
* - :concept:`read_only_sequence`
- :var:`Seq` is read-only
* - :concept:`const_iterable_sequence`
- :var:`Seq` is const-iterable and :var:`Func` is const-invocable

:see also:

* :func:`flux::filter_map`

``filter_map``
^^^^^^^^^^^^^^

.. function::
template <sequence Seq, typename Func> \
requires std::invocable<Func&, element_t<Seq>> && \
optional_like<std::invoke_result_t<Func&, element_t<Seq>>> \
auto filter_map(Seq seq, Func func) -> sequence auto;

Performs both filtering and mapping using a single function.
Given a unary function :var:`func` returning an "optional-like" type, the returned adaptor filters out those elements for which :var:`func` returns a "disengaged" optional (that is, those which return :texpr:`false` after conversion to :texpr:`bool`). It then dereferences the remaining elements using :expr:`operator*`.
Equivalent to::

map(seq, func)
.filter([](auto&& arg) { return static_cast<bool>(arg) })
.map([](auto&& arg) -> decltype(auto) { return *std::forward(arg); });

:models:

.. list-table::
:align: left
:header-rows: 1

* - Concept
- When
* - :concept:`multipass_sequence`
- :var:`Seq` is multipass
* - :concept:`bidirectional_sequence`
- :var:`Seq` is bidirectional
* - :concept:`random_access_sequence`
- Never
* - :concept:`contiguous_sequence`
- Never
* - :concept:`bounded_sequence`
- :var:`Seq` is bounded
* - :concept:`sized_sequence`
- Never
* - :concept:`infinite_sequence`
- :var:`Seq` is infinite
* - :concept:`read_only_sequence`
- :var:`Seq` is read-only
* - :concept:`const_iterable_sequence`
- :var:`Seq` is const-iterable and :var:`Func` is const-invocable

``flatten``
^^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions include/flux.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <flux/op/equal.hpp>
#include <flux/op/fill.hpp>
#include <flux/op/filter.hpp>
#include <flux/op/filter_map.hpp>
#include <flux/op/find.hpp>
#include <flux/op/find_min_max.hpp>
#include <flux/op/flatten.hpp>
Expand Down
12 changes: 12 additions & 0 deletions include/flux/core/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,18 @@ template <typename T>
requires detail::has_nested_sequence_traits<T>
struct sequence_traits<T> : T::flux_sequence_traits {};

namespace detail {

template <typename O>
concept optional_like =
std::default_initializable<O> &&
std::movable<O> &&
requires (O& o) {
{ static_cast<bool>(o) };
{ *o } -> flux::detail::can_reference;
};

}

} // namespace flux

Expand Down
9 changes: 9 additions & 0 deletions include/flux/core/inline_sequence_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ struct inline_sequence_base {
[[nodiscard]]
constexpr auto filter(Pred pred) &&;

template <typename Func>
requires std::invocable<Func&, element_t<Derived>> &&
detail::optional_like<std::invoke_result_t<Func&, element_t<Derived>>>
[[nodiscard]]
constexpr auto filter_map(Func func) &&;

[[nodiscard]]
constexpr auto filter_deref() && requires detail::optional_like<value_t<Derived>>;

[[nodiscard]]
constexpr auto flatten() && requires sequence<element_t<Derived>>;

Expand Down
9 changes: 0 additions & 9 deletions include/flux/core/simple_sequence_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ struct simple_sequence_base : inline_sequence_base<D> {};

namespace detail {

template <typename O>
concept optional_like =
std::default_initializable<O> &&
std::movable<O> &&
requires (O& o) {
{ static_cast<bool>(o) };
{ *o } -> flux::detail::can_reference;
};

template <typename S>
concept simple_sequence =
std::derived_from<S, simple_sequence_base<S>> &&
Expand Down
72 changes: 72 additions & 0 deletions include/flux/op/filter_map.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@

// Copyright (c) 2024 Tristan Brindle (tcbrindle at gmail dot com)
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#ifndef FLUX_OP_FILTER_MAP_HPP_INCLUDED
#define FLUX_OP_FILTER_MAP_HPP_INCLUDED

#include <flux/core.hpp>
#include <flux/op/map.hpp>

namespace flux {
Guekka marked this conversation as resolved.
Show resolved Hide resolved

namespace detail {

struct filter_map_fn {
// If dereffing the optional would give us an rvalue reference,
// prevent a probable dangling reference by returning by value instead
template <typename T>
using strip_rvalue_ref_t = std::conditional_t<
std::is_rvalue_reference_v<T>, std::remove_reference_t<T>, T>;

template <adaptable_sequence Seq, typename Func>
requires (std::invocable<Func&, element_t<Seq>> &&
optional_like<std::remove_cvref_t<std::invoke_result_t<Func&, element_t<Seq>>>>)
constexpr auto operator()(Seq&& seq, Func func) const
{
return flux::map(FLUX_FWD(seq), std::move(func))
.filter([](auto&& opt) { return static_cast<bool>(opt); })
.map([](auto&& opt) -> strip_rvalue_ref_t<decltype(*FLUX_FWD(opt))> {
return *FLUX_FWD(opt);
});
}
};

} // namespace detail

FLUX_EXPORT inline constexpr auto filter_map = detail::filter_map_fn{};

template <typename D>
template <typename Func>
requires std::invocable<Func&, element_t<D>> &&
detail::optional_like<std::invoke_result_t<Func&, element_t<D>>>
constexpr auto inline_sequence_base<D>::filter_map(Func func) &&
{
return flux::filter_map(derived(), std::move(func));
}

namespace detail
{

struct filter_deref_fn {
template <adaptable_sequence Seq>
requires optional_like<value_t<Seq>>
constexpr auto operator()(Seq&& seq) const
{
return filter_map(FLUX_FWD(seq), [](auto&& opt) -> decltype(auto) { return FLUX_FWD(opt); });
}
};

} // namespace detail

FLUX_EXPORT inline constexpr auto filter_deref = detail::filter_deref_fn{};

template <typename D>
constexpr auto inline_sequence_base<D>::filter_deref() && requires detail::optional_like<value_t<D>>
{
return flux::filter_deref(derived());
}
} // namespace flux

#endif
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ add_executable(test-flux
test_equal.cpp
test_fill.cpp
test_filter.cpp
test_filter_map.cpp
test_find.cpp
test_find_min_max.cpp
test_flatten.cpp
Expand Down
137 changes: 137 additions & 0 deletions test/test_filter_map.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@

// Copyright (c) 2024 Tristan Brindle (tcbrindle at gmail dot com)
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#include "catch.hpp"

#include <array>
#include <optional>
#include <utility>
Guekka marked this conversation as resolved.
Show resolved Hide resolved

#include "test_utils.hpp"

namespace {

constexpr auto is_even_opt = [](int i) { return i % 2 == 0 ? std::optional{i} : std::nullopt; };

struct Pair {
int i;
bool ok;

[[nodiscard]] constexpr auto map_if_ok() const { return ok ? std::optional{*this} : std::nullopt; }

constexpr bool operator==(const Pair&) const = default;
};

using filter_fn = decltype(flux::filter_map);

// int is not a sequence
static_assert(not std::invocable<filter_fn, int, decltype(is_even_opt)>);
// int is not a function
static_assert(not std::invocable<filter_fn, int(&)[10], int>);
// "func" does not return optional_like
static_assert(not std::invocable<filter_fn, int(&)[10], decltype([](int) {})>);
// Incompatible predicate
static_assert(not std::invocable<filter_fn, int(&)[10], decltype([](int*) { return true; })>);

constexpr bool test_filter()
{
// Basic filtering
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto filtered = flux::filter_map(flux::ref(arr), is_even_opt);
using F = decltype(filtered);
static_assert(flux::sequence<F>);
static_assert(flux::bidirectional_sequence<F>);
static_assert(flux::bounded_sequence<F>);
static_assert(not flux::ordered_cursor<F>);
static_assert(not flux::sized_sequence<F>);

static_assert(flux::sequence<F const>);
static_assert(flux::bidirectional_sequence<F const>);
static_assert(flux::bounded_sequence<F const>);
static_assert(not flux::ordered_cursor<F const>);
static_assert(not flux::sized_sequence<F const>);

STATIC_CHECK(check_equal(filtered, {0, 2, 4, 6, 8}));
STATIC_CHECK(check_equal(std::as_const(filtered), {0, 2, 4, 6, 8}));
}

// A predicate that always returns true returns what it was given
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto filtered = flux::filter_map(flux::ref(arr), [](auto&& i) { return std::optional{i}; });

STATIC_CHECK(check_equal(arr, filtered));
}

// A predicate that always returns false returns an empty sequence
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto filtered = flux::filter_map(flux::ref(arr), [](auto&&) -> std::optional<int> { return std::nullopt; });

STATIC_CHECK(filtered.is_empty());
}

// We can use any optional_like such as a pointer
{
int i = 1, j = 3;
std::array<int *, 4> arr{&i, nullptr, &j, nullptr};

auto filtered = flux::filter_map(arr, [](auto ptr) { return ptr; });

STATIC_CHECK(check_equal(filtered, {1, 3}));
}

// ... Better expressed as filter_deref
{
int i = 1, j = 3;
std::array<int *, 4> arr{&i, nullptr, &j, nullptr};

auto filtered = flux::filter_deref(arr);

STATIC_CHECK(check_equal(filtered, {1, 3}));
}

// We can use a PMF to filter_map
{
std::array<Pair, 4> pairs = {
Pair{1, true},
{2, false},
{3, true},
{4, false}
};

auto f = flux::filter_map(pairs, &Pair::map_if_ok);

STATIC_CHECK(check_equal(f, {Pair{1, true}, Pair{3, true}}));
}

// Reversed sequences can be filtered
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto filtered = flux::ref(arr).reverse().filter_map(is_even_opt);

STATIC_CHECK(check_equal(filtered, {8, 6, 4, 2, 0}));
}

// ... and filtered sequences can be reversed
{
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto filtered = flux::filter_map(flux::ref(arr), is_even_opt).reverse();

STATIC_CHECK(check_equal(filtered, {8, 6, 4, 2, 0}));
}

return true;
}
static_assert(test_filter());

}

TEST_CASE("filter_map")
{
bool result = test_filter();
REQUIRE(result);
}