C++0x Concepts had a feature Concept Maps that allowed a set of functions, types, and template definitions to be associated with a concept and the map to be specialized for types that meet the concept.
This allowed open extension of a concept.
A definition could be provided that allows an algorithm to operate in terms of the API a concept presents and the map would define how those operations are implemented for a particular type.
typeclass works.
Concepts-Lite
The feature was very general, and lost as part of the Concepts-Lite proposal that was eventually adopted.
This loss of a level of indirection means that the APIs for a concept must be implemented by those names for a type, even when those names are not particularly good choices in the natural domain of a type rather than in the domain as a concept.
The proliferation of transform functions for functorial map is such a problem.
It is also a problem when adapting types that are closed for extension or do not permit member functions.
Don't know if you should
Virtual Interface
Adapters
class student record { public: string id; string name; string address; bool id_equal(const student record&); bool name_equal(const student record&); bool address_equal(const student record&); };
concept_map EqualityComparable<student record>{ bool operator==(const student record& a, const student record& b){ return a.id_equal(b); } };
Very useful for pointers
concept_map BinaryFunction<int (*)(int, int), int, int> { typedef int result_type; };
Let's not go there right now.
trait PartialEq { fn eq(&self, rhs: &Self) -> bool; fn ne(&self, rhs: &Self) -> bool { !self.eq(rhs) } }
namespace N::hidden { template <typename T> concept has_eq = requires(T const& v) { { eq(v, v) } -> std::same_as<bool>; }; struct eq_fn { template <has_eq T> constexpr bool operator()(T const& x, T const& y) const { return eq(x, y); } }; template <has_eq T> constexpr bool ne(T const& x, T const& y) { return not eq(x, y); } template <typename T> concept has_ne = requires(T const& v) { { ne(v, v) } -> std::same_as<bool>; }; struct ne_fn { template <has_ne T> constexpr bool operator()(T const& x, T const& y) const { return ne(x, y); } }; } // namespace N::hidden
See Why tag_invoke is not the solution I want by Barry Revzin https://brevzin.github.io/c++/2020/12/01/tag-invoke/
namespace N { inline namespace function_objects { inline constexpr hidden::eq_fn eq{}; inline constexpr hidden::ne_fn ne{}; } // namespace function_objects template <typename T> concept partial_equality requires(std::remove_reference_t<T> const& t) { eq(t, t); ne(t, t); }; } // namespace N
See Why tag_invoke is not the solution I want by Barry Revzin https://brevzin.github.io/c++/2020/12/01/tag-invoke/
Tied to the type system
Automatable
no virtual calls
Adds a record to the function that defines the operations for the type.
Can we do that?
Templates!
Avoid ADL
Object Lookup rather than Overload Lookup
Variable templates have become more powerful
We can have entirely distinct specializations
template <class T> concept partial_equality = requires( std::remove_reference_t<T> const& t) { { partial_eq<T>.eq(t, t) } -> std::same_as<bool>; { partial_eq<T>.ne(t, t) } -> std::same_as<bool>; };
partial_eq<T>template<class T> constexpr inline auto partial_eq = hidden::partial_eq_default;
constexpr inline struct partial_eq_default_t { constexpr bool eq(has_eq auto const& rhs, has_eq auto const& lhs) const { return (rhs == lhs); } constexpr bool ne(has_eq auto const& rhs, has_eq auto const& lhs) const { return (lhs != rhs); } } partial_eq_default;
has_eq
template <typename T> concept has_eq = requires(T const& v) { { operator==(v, v) } -> std::same_as<bool>; };
In a bit
A little more than you think.
The similarity to left and right fold is NOT an accident
Note that it's self-referential
This is common
class Semigroup a => Monoid a where mempty :: a mempty = mconcat [] mappend :: a -> a -> a mappend = (<>) mconcat :: [a] -> a mconcat = foldr mappend mempty
\(empty \, | \, concat\)
template <typename T, typename M> concept MonoidRequirements = requires(T i) { { i.identity() } -> std::same_as<M>; } || requires(T i, std::ranges::empty_view<M> r1) { { i.concat(r1) } -> std::same_as<M>; };
I am ignoring all sorts of const volatile reference issues here.
template <class Impl> requires MonoidRequirements< Impl, typename Impl::value_type> struct Monoid : protected Impl { auto identity(this auto&& self); template <typename Range> auto concat(this auto&& self, Range r); auto op(this auto&& self, auto a1, auto a2); };
empty is a terrible name, concat only a little better. empty becomes identity
identityauto identity(this auto && self) { std::puts("Monoid::identity()"); return self.concat(std::ranges::empty_view<typename Impl::value_type>{}); }
concattemplate<typename Range> auto concat(this auto&& self, Range r) { std::puts("Monoid::concat()"); return std::ranges::fold_right(r, self.identity(), [&](auto m1, auto m2){return self.op(m1, m2);}); }
opauto op(this auto&& self, auto a1, auto a2) { std::puts("Monoid::op"); return self.op(a1, a2); }
this and CRTPWe'll see in a moment, but it's because we want to constraint the required implementation.
We want to use the derived version which has all of the operations.
Plustemplate <typename M> class Plus { public: using value_type = M; auto identity(this auto&& self) -> M { std::puts("Plus::identity()"); return M{0}; } auto op(this auto&& self, auto s1, auto s2) -> M { std::puts("Plus::op()"); return s1 + s2; } };
PlusMonoidMaptemplate<typename M> struct PlusMonoidMap : public Monoid<Plus<M>> { using Plus<M>::identity; using Plus<M>::op; };
Need to pull the operations from the Monoid instance into the Map, so we get the right ones being used by concat.
This might be simpler if we didn't allow choice of the basis operations, but that's also overly restrictive.
template<class T> auto monoid_concept_map = std::false_type{}; template<> constexpr inline auto monoid_concept_map<int> = PlusMonoidMap<int>{}; template<> constexpr inline auto monoid_concept_map<long> = PlusMonoidMap<long>{}; template<> constexpr inline auto monoid_concept_map<char> = PlusMonoidMap<char>{};
concat instead?class StringMonoid { public: using value_type = std::string; auto op(this auto&&, auto s1, auto s2) { std::puts("StringMonoid::op()"); return s1 + s2; } template <typename Range> auto concat(this auto&& self, Range r) { std::puts("StringMonoid::concat()"); return std::ranges::fold_right( r, std::string{}, [&](auto m1, auto m2) { return self.op(m1, m2); }); } };
No, I'm not properly constraining Range here. No, I'm not actually recommending this as an implementation.
struct StringMonoidMap : public Monoid<StringMonoid> { using StringMonoid::op; using StringMonoid::concat; }; template<> constexpr inline auto monoid_concept_map<std::string> = StringMonoidMap{};
template<typename P> void testP() { auto d1 = monoid_concept_map<P>; auto x = d1.identity(); assert(P{} == x); auto sum = d1.op(x, P{1}); assert(P{1} == sum); std::vector<P> v = {1,2,3,4}; auto k = d1.concat(v); assert(k == 10); }
std::cout << "\ntest int\n"; testP<int>(); std::cout << "\ntest long\n"; testP<long>(); std::cout << "\ntest char\n"; testP<char>();
std::string
This will use the StringMonoid we defined a few moments ago.
auto d2 = monoid_concept_map<std::string>; std::cout << "\ntest string\n"; auto x2 = d2.identity(); assert(std::string{} == x2); auto sum2 = d2.op(x2, "1"); assert(std::string{"1"} == sum2); std::vector<std::string> vs = {"1","2","3","4"}; auto k2 = d2.concat(vs); assert(k2 == std::string{"1234"});
Note that the map type is mostly invisible.
Plus::identity() Plus::op() Monoid::concat() Plus::identity() Plus::op() Plus::op() Plus::op() Plus::op()
Plus::identity() Plus::op() Monoid::concat() Plus::identity() Plus::op() Plus::op() Plus::op() Plus::op()
Plus::identity() Plus::op() Monoid::concat() Plus::identity() Plus::op() Plus::op() Plus::op() Plus::op()
Monoid::identity() StringMonoid::concat() StringMonoid::op() StringMonoid::concat() StringMonoid::op() StringMonoid::op() StringMonoid::op() StringMonoid::op()
Folding is very much tied to Range like things.
It can, and has, been generalized to things that can be traversed.
monoids are still critical for Traversables.
If the summary type is monoidal, nodes can hold summaries of all the data below them.
fingertrees Much of the flexibility of fingertrees comes from the monoidal tags.
They are also fairly complicated.
Technique can be applied to other, simpler trees.
P3200 (eventually) ((C++29))
Simplified tree with data at the edges
Show the monoid-map branch of
Tell you what I told you
Or comments