The Road to Flux 1.0 #242
Replies: 3 comments 9 replies
-
Don't take this the wrong way, but this seems such a backwards way of thinking. You're consciously deciding to sacrifice readability, discoverability, diagnostic quality, and compilation speed. Why? Why does it matter if your work ends up in the Standard Library or not, or why does it matter whether people find it more or less familiar to ranges? Just because something got in the C++ standard it doesn't mean that it's the best solution to a given problem. The committee is made up of human beings. I prefer using I can imagine people having a similar opinion for Perhaps your time and effort might be better spent moving away from conventions that cause real issues in Standard Library components, rather than following in their footsteps. Perhaps you'd get smaller adoption of your library, but -- ultimately -- what is your goal? |
Beta Was this translation helpful? Give feedback.
-
|
These are just my two cents as a naive, external observer and an occasional user of C++. The entire identity of Flux to me was as the "this is the same thing as range-v3 but with normal syntax" library. If it's going to be the same as std::ranges, then frankly, I might as well just use std::ranges instead. All the things Flux does better with its iterator model and implementation details will probably make it into ranges in some form in C++32 anyway, and I'll get those benefits automatically. To use an analogy, I firmly believe that most people liked and adopted fmtlib simply because it did formatting "the normal way". It's safer, more modern, more performant and all of that, sure - but let's be real, nobody cared about that. There's no shortage of anecdotes and comments out there from C++ developers saying that they prefer to use old-school This change is akin to fmtlib deciding that implementing fmt::cout << "text" << fmt::f << a << fmt::reset << "text" << fmt::gh << b << fmt::reset << fmt::endl;in the library instead. When I first saw range-v3 and std::ranges, I couldn't comprehend it. Literally. My brain did not understand what it was looking at. The syntax made no sense to me. I read guides and tutorials on how you're meant to use it, what it's meant to do, and how it's meant to work. None of it helped. It all went in one ear, and out the other. So, I discarded it as a libary that does advanced and pointless things which I probably don't need, and continued happily programming without it for the next 5-6 years. As I progressed further in my programming journey though, chained To this day, the way I use the library is by rewriting every "Okay, so we want Of course, I GET why it's this way. I get why Eric Niebler decided to use overloaded pipe operators, and I get why you did. I understand iterators now, I know what functional programming is, I know what the language's limitations are, I get why it makes chaining custom user functions hard. Hell, I've tried writing my own wrapper library around ranges to enable member operator syntax before, and it makes perfect sense why somebody would implement things this way. But nobody else cares about that. End users and people new to the language don't care. They will take one look at your I can't shake the feeling that this will push the needle in the wrong direction. UFCS or some form of extension methods are an essential feature which just about every other programming language has had for decades, and something C++ desperately needs to have (as this library continues to prove). But now, both ranges and Flux will be brought up as arguments as to why we should keep not adding it. After all, both ranges and Flux got by just fine without UFCS or extension methods. Just use And all the while, C++ continues accruing this reputation of being a horrible, unergonomic, hellish language that nobody wants to work with, while the popularity of other, crab-adjacent languages keeps shooting upwards. |
Beta Was this translation helpful? Give feedback.
-
|
I don't have a preference on the
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Flux has been around for a couple of years now, and the feedback I get is mostly that people seem to think it's pretty good. I think it's pretty good too! But I'm also aware that the current design of the library has some shortcomings. Over the last few months I've spent a lot of time thinking about ways in which things can be improved, and experimenting with various alternative approaches.
Having settled on what I believe is a promising design for the library going forward, I wanted to give an update on the planned changes as we move towards Flux 1.0. I'm very keen to get feedback from Flux users (and potential users!) -- so if you have any thoughts or comments on the new design, please let me know!
There's quite a lot here, so please make yourself comfortable, grab a
${PREFERRED_HOT_BEVERAGE}and we'll dive in...Motivation
I guess I should make clear that the goals for Flux haven't changed: as ever, I want to offer broadly the same feature set as std::ranges, but in a way that is:
Indeed, the changes proposed here are about furthering these goals.
One piece of feedback I've received repeatedly is that while people generally like Flux when they try it out (especially its performance vs ranges), they tend to find that it feels "too different" to the standard library. Flux does many things its own way, making it feel like there's a lot to learn. This can make it a tough sell, especially when people have already invested the time in learning about ranges.
One of the goals for Flux 1.0 therefore is to reduce the "diff" from standard C++ -- that is, to try to make Flux look and feel a bit more familiar.
Another piece of feedback I've received several times is that people would love to see either internal iteration or the cursor model (or both) proposed for standardisation. While I (obviously) agree that these would be excellent, with my SG9 hat on it would be an incredibly tough sell to bring the current Flux
sequencemodel to the committee. It is in a sense both too large and too similar to C++20 ranges.With this in mind, the next version of Flux will replace the existing
sequenceconcept with two separate (but complementary) interfaces: one for single-pass algorithms, using internal iteration (iterable) and a more powerfulcollectionconcept (a simplified version of today'smultipass_sequence).All begin well, I hope to propose
iterablefor standardisation in the C++29 cycle, for which Flux will serve as a reference implementation. With luck, the reduced scope (and performance and safety benefits) will help convince LEWG that this would be a useful addition to the standard.Planned changes
Pipes, not dots
Reluctantly, we'll move from using member functions for chaining to overloading
operator|, as in ranges and the forthcoming stdexec. This is worse for readability, worse for discoverability, worse for error messages and worse for compile times than using member functions -- but it's what the standard library does, and so we're going to follow suit.[EDIT: This was a bit of an oversimplification that doesn't take into account that there are other benefits to using pipes -- see this comment for more details.]
This means that Flux pipelines will change from today's
to
(Yes, it would be possible to support both syntaxes, but I'd rather not do that -- it would mean more code that needed to be maintained and tested, and would lead to confusion about which is the "proper" way to write pipelines. Let's just do it the standard way, even if that's not ideal.)
Note that, unlike ranges and stdexec (but like Range v3's actions) I plan on making terminal algorithms like
sum()pipeable, just because I think it reads quite nicely. I'd also quite like to add these pipeable function objects to a nested inline namespace (tentatively,flux::operations?) so that you could say:to avoid repeating the namespace name, but without bringing everything in namespace
fluxinto scope (rather likeusing namespace std::literals).One thing that won't be changing is the ownership model for adaptors. Flux adaptors will continue to act like sinks, taking ownership of what you pass to them -- rather than the confusing situation with range views today, where they sometimes transfer ownership and sometimes act as reference types. (We'll also continue to reject non-trivially-copyable lvalues, so you'll have to explicitly copy things like
vectors into an adaptor.)This means that, as ever, you'll need to be explicit if you want to pass a sequence reference into a Flux adaptor. The best way to do this will be with
std::reforstd::cref-- the currentflux::refandflux::mut_reffunctions will be retired in favour of using the standard functions.Goodbye
sequence, helloiterableBackground
STL input iterators have always been a bit weird. For forward ranges, an iterator acts like a pointer to a range element; but for single-pass ranges this abstraction falls apart. Trying to use iterators with single-pass ranges is the proverbial square peg in a round hole, and has always felt that way in practice.
The same is true of cursors in Flux today. Cursors simply aren't a good fit for single-pass iteration. I was aware of this when I first started working on the library, but I felt that it was better to try to come up with a single, generalised interface for all sequences, and that perfect was the enemy of good. With the benefit of hindsight, I've come to think that trying for a single, universal interface was probably the wrong decision.
A position-based interface (be it cursors or iterators) naturally means separating the "read" and "advance" operations. This is ideal for multipass sequences, where elements are readily accessible in memory or can be cheaply recreated. But single-pass sequences are isomorphic to streams, where in most cases "read and advance" is more naturally implemented as single operation. This mismatch is how we end up with things like the notoriously poor codegen for
views::filter(), the infamoustransform | filter"terrible problem", and the "take(5)" issue for stream wrappers.Separately, when I started working on Flux, internal iteration was something of an afterthought -- I noticed it was possible to add it as a customisation point, and so I did so. It was only later that I realised how important this is for performance -- and that what most people like best about Flux happens because they're using code-paths where the internal iteration optimisation kicks in.
So, given that the current single-pass iteration scheme isn't very good, and internal iteration is great, we're going to start again, and introduce a new single-pass iteration model based around internal iteration.
Iteration contexts
At the heart of the new model is what's called an
iteration_context. If we had proper concepts in the language, theiteration_contextinterface would be defined like this:That is, an iteration context needs to provide an
element_typetypedef, and arun_whilemember function which takes a unary predicate and returns aniteration_result.When
run_whileis called, the context invokes the user's predicate with each element of the sequence in succession; if the predicate returns false, thenrun_whilestops and returnsiteration_result::incomplete. Otherwise, when the context reaches the end of the sequence, it returnsiteration_result::complete. Ifrun_whilereturnsincomplete, it is expected that we can immediately callrun_whileagain to restart iteration from the next element.Iteration contexts will typically hold pointers or references to their parent sequences, and are expected to be non-copyable and non-movable -- this both discourages people from using them as anything except short-lived local variables, and saves implementors from having to guarantee validity after a move (which might be tricky in some cases).
Because iteration contexts hold references, they should be considered as "borrowing" from their parent sequence, to use Rust terminology. This means that, for safety, we should adhere to the so-called "law of exclusivity": we must not call a mutating function on the sequence while an iteration context is "live", and if we have a mutating iteration context then we should avoid any use of the parent sequence. One way of thinking about this is as if the sequence gets moved into the context when we create it, and restored in the context destructor.
The simplest possible iteration context might be something like so:
This infinitely generates the number 4 until the user is fed up enough to return
falsefrom their predicate.A more useful example is the following:
This is a generic iteration context for any
forward_range. A generic context for anyinput_rangeis only very slightly more complicated, since we don't have postfixoperator++on iterators in that case.Algorithms on iteration contexts are as follows:
run_whileJust returns
ctx.run_while(pred)stepPerforms a single iteration step and calls
funcwith the next sequence element, wrapping the return value offuncin an optional. For example, given ourfourscontext above, we could writeFormally, let
Rbeinvoke_result_t<Func&, Ctx::element_type>. Ifis_void_v<R>is true,stepis equivalent toOtherwise, let
Sbestd::decay_t<R>ifRis an rvalue reference type, orRotherwise. Thenstepis equivalent to:next_elementEquivalent to
step(ctx, std::identity{})-- that is, it returns an optional containing the next sequence element, or anulloptif there were no more elements.This function allows single-step iteration through an iterable sequence, equivalent to the Rust iteration model.
If the element_type is a reference then care must be taken with this function, as the reference may be invalidated by a subsequent call to
run_while,stepornext_element.Iterables
While iteration contexts actually perform the low-level operations, one of the design goals is that they should mostly be considered an implementation detail. Most users won't interact with iteration contexts directly. Rather, the intention is that end users will compose pipelines out of higher-level algorithms on iterables -- that is, things we can iterate over.
The
iterableconcept is defined as:i.e. it is satisfied when
flux::iterate(it)is valid and returns aniteration_context. In turn,flux::iterate(it)returnsflux::iterable_traits<R>::iterate(it), whereRisstd::remove_cvref_t<decltype(it)>, ifiterable_traits<R>is not generated from the primary templateit.iterate()if that is a valid function callrange_iteration_contextifdecltype(it)satisfiesinput_rangeIf none of these apply then the call to
iterate()is ill-formed.Translated into english, this means that we're
iterableif we have a member functioniterate()which returns aniteration_context, or if we provide a specialisation offlux::iterable_traitsalong the lines ofIt's worth noting here that many iterables will provide both const and non-const overloads of
iterate(), returning different context types. Unlike STL iterators however, there is no requirement for convertibility between non-const and const iteration contexts.The
iterableconcept is equivalent to today's single-passsequence. The plan for Flux is that all of the current single-pass algorithms and adaptors will be modified to operate oniterables instead.For example,
for_eachcan be implemented on iterables as:while
fold()can be written as roughly:Algorithms which operate on multiple sequences in lock-step generally can't be written directly in terms of
run_while; instead, we can usestepornext_elementto advance each sequence by a single element at a time. For example, here is how we could writeequal:As ever, writing iterable adaptors involves a bit more work than terminal algorithms, but generally less so than today's
sequenceadaptors (and much less so than most C++20 range adaptors). Typically, an adaptor will provide an iteration context which wraps the context of its base iterable. For example, themapadaptor (transform_viewin C++20) would look something like this:Reverse iteration
Reverse iteration in Flux today works in almost exactly the same was as it does in the STL. We have a
bidirectional_sequenceconcept which "inherits from"multipass_sequenceand allows the user to decrement cursors. We then have areverse()adaptor which swaps over the increment and decrement operations on cursors, with some fiddly off-by-one messing about in the read operation (again, just like reverse iterators).Happily, the new iterable model allows us to make a dramatic simplification to this arrangement.
We can define a
reverse_iterableconcept aswhere
flux::reverse_iterate(it)is defined similarly toiterate(). In other words, we require thatreverse_iterate()returns an iteration context -- one that just happens to generate the elements from back to front.With this concept in place, we can provide a generic reverse-iterating context for all bidirectional, common ranges:
It's worth noting that unlike
std::reverse_iterator, this formulation doesn't need to perform an extra decrement before each dereference, reducing the number of per-element operations and likely allowing more optimisation opportunities. Also, importantly, it means that iterables can be reversible without being multipass, unlike the STL and current Flux models.Most adaptors can support reverse iteration without too much trouble. For example, our
map_adaptorabove requires very little modification:Providing a
reverse_adaptor(likestd::views::reverse) is also very simple -- we just switch over the calls toiterateandreverse_iterate:Iterables and ranges
As we've seen, under the new model, every C++20
input_rangeis automatically aflux::iterable, and every bidirectional common range isreverse_iterable. This is a huge improvement on the status quo, where only sized, contiguous ranges are automatically Fluxsequences.This means that going from the ranges world to the Flux world couldn't be easier. You can pass any C++20 range into a Flux algorithm, exactly as you would for a
std::rangesalgorithm. You can also pass any range into a compatible Flux adaptor, without needing to do anything special. With the switch to pipes as well, this means that Flux will (hopefully) become a near drop-in replacement for view pipelines, but with improved performance in many common cases.Going in the opposite direction, from the Flux iterable world to ranges, is a little bit trickier, for a couple of reasons:
What this means is that we need to use an adaptor class -- a range object -- to hold both the iteration context and the cached element. This range can then hand out input-only iterators, like so:
Users can then move from Flux to ranges by piping into
as_range, which returns anas_range_adaptor. For example:One thing that's worth noting is that while
rnghere is a range whose elements are lazily generated, it is not aview, because views are required to be at least moveable. This is turn means that we can't pass an rvalue "as_range" directly into a C++20 view function -- we need to save it as a named variable first, and then pass it as an lvalue:While this is somewhat unfortunate, the fact that this only applies to single-pass iterables, there is an easy workaround, and Flux provides a wealth of adaptors of its own, will hopefully mean that it doesn't come up too often in practice.
Special consideration: the range-based for loop
Probably the most common reason that people will want to convert iterables to ranges is so that they can use a range-based for loop. While they can do so with
as_range, it's a bit annoying for users to need to remember to add that to the end of their Flux pipelines in order to use the convenient built-in syntax.Fortunately there is something we can do. It turns out that if you look at the way the range-based for loop desugars, the requirements on the "iterator" returned by
begin()are much less strict than the C++20std::rangesconcepts. In particular, there is no requirement that such "iterators" be moveable or copyable.This means that we could, in theory, add
begin()andend()methods to Flux iterable adaptors which return a non-moveable "pseudo-iterator", specifically for use with range-for loops:This would mean that we could use range-based for loops "naturally":
The downside of this approach is that it might be confusing for users if
.begin()is valid to call, and returns something that looks a lot like an iterator, but won't work with C++20 (or many C++17) algorithms without usingas_range.(Of course, the ideal solution to this problem would be to teach the language for loop about the
iterableconcept. Maybe one day...)Collections
As we've mentioned, the
iterableconcept is all about single-pass iteration, and it turns out that you can do an awful lot just with this. But some algorithms require that we are able to visit the same location in a sequence more than once -- that is, multi-pass iteration.The current version of Flux uses the
multipass_sequenceconcept for this purpose. In the next version, this will be replaced by the much simplifiedcollectioninterface instead.Positions
In Flux today, a
cursorfor a multipass sequence represents a position in that sequence, rather like a generalisation of an array index. (This is in contrast to iterators, which are a generalisation of array pointers.)In moving to
collection, what a cursor represents won't change -- but the name will. Freed from the need to support single-pass sequences (where there is no meaningful notion of "place"), we can switch to calling cursors what they really are: positions.As with multipass cursors in today's Flux, collection positions are required to model
std::regular-- that is, they must be default constructible, copyable, and equality comparable. We are also adding an addition requirement on positions: they must have a strict total order.In other words, given two positions
p1andp2, as well as being able to ask (as ever) whetherp1 == p2, we also want to be able to ask whetherp1 < p2, that is, ifp1represents an earlier position in a collection thanp2.The goal for this is safety. If positions are always ordered, then we can easily perform bounds checking for all collections. In particular, it means that we can also perform bounds checking for arbitrary slices of collections. This is particularly important if different mutable slices get passed to different threads, where it is vital that we ensure that each thread does not write beyond its assigned bounds.
Formally, the
positionconcept looks like this:Collection concept
In order to define a type as a
collection, users need to provide implementations of four functions. As with theiterableconcept, these can either be member functions of the type itself, or they can be static members of a user-defined specialisation offlux::collection_traits<T>.The required functions are:
begin_posAs you might expect, returns the start
positionof the collection.end_posThis returns the end position of the collection, which must be the same type as
begin_posinc_posIncrements the given position so that it points to the next element of the collection.
read_at_uncheckedReturns the element at the given position. The "unchecked" part means that the implementor may assume that
is_readable_position(see below) istrueon entry to this function -- that is, someone else has already taken care of bounds checking. This allows us to implement things liketry_read_at()without double bounds checks.One thing to note is that the first three of these functions take the collection by const reference. This is a change from the existing
multipass_sequencerequirements, with the goal of making life easier for people implementing the collection protocol, as well as making things conceptually simpler overall. As always, it is possible (and common) for collections to provide tworeadoverloads for const- and non-const element access.Another thing to note is that all collections are required to be able to provide an end position. This means that, unlike iterables, collections are always finite. (I don't believe it's possible to write an infinite collection with ordered positions without some sort of
BigInttype anyway, but if I'm wrong about that please let me know.) The current Fluxbounded_sequenceconcept is no more, as all collections are now "bounded".We can now define some further collection algorithms, using the above functions:
next_posReturns the next position after
posas a functional update. Equivalent tois_valid_positionEquivalent to
pos >= begin_pos(col) && pos <= end_pos(col)is_readable_positionEquivalent to
pos >= begin_pos(col) && pos < end_pos(col)(Note that
end_pos(c)is a valid but not readable position.)read_atPerforms bounds-checked read access. This should be the default read operation for collections.
Equivalent to
except that bounds checking may be performed at compile time if possible (see this blog post for details).
try_read_atIf
is_readable_pos(col, pos)is true, returns an optional containing the result ofread_at_unchecked(col, pos). Otherwise, returns a disengaged optional.We can also implement a generic iteration context for collections:
Thus every collection can be iterable, and indeed the
collectionconcept subsumesiterable. Of course, the above context type is just used as the fallback default; individualcollectionimplementations may chose to provide a customiterableimplementation instead.Collection refinements
Permutable collection
A permutable collection additionally implements an extra customisation point,
swap_at_unchecked:As you might imagine, this swaps the elements at the two positions, with the assumption that both positions are readable. A default implementation of this function is provided when
collection_element_t<C>is a non-const lvalue reference.Of course, we also provide a bounds-checking wrapper, which should be preferred:
The purpose of this function is so that we can implement in-place algorithms like
rotate()andswap()on collections which return proxy references, likezip.(It will also be necessary if we ever get a borrow checker for C++...)
Bidirectional collection
A collection can be made bidirectional by providing a
dec_posimplementation which, unsurprisingly, decrements a position. We also provide aprev_posfunction as a counterpart tonext_pos. Obviously, all bidirectional collections are alsoreverse_iterable.[Indeed, now that we have
reverse_iterableit might be worth looking at whetherbidirectional"pulls its weight" in the concept hierarchy.]Random-access collection
A random-access collection additionally provides implementations of two more customisation points:
distanceReturns the number of elements between
fromandto, which may be negative ifto < from, in constant time. The size of a random-access collection can be found withdistance(c, begin_pos(c), end_pos(c))offset_posAdjusts
posbyoffsetplaces (which may be negative) in constant time.If
offset > distance(col, pos, end_pos(col))oroffset < distance(col, pos, begin_pos(col))then the resulting position will be invalid. It is unspecified whether this function raises a runtime error in this case.[Note: some implementations may need to catch an out-of-bounds jump immediately, to prevent undefined behaviour. Other implementations may choose to defer the bounds check until the next attempted read.]
Contiguous collection
A contiguous collection is a random-access collection whose elements are stored in a contiguous array in memory.
The
contiguous_collectionconcept requires that the collection's element type is an lvalue reference, and that the collection implements theas_spancustomisation point:as_spanReturns a
spanover the elements ofcol.[Note:
contiguous_rangeand the current Fluxcontiguous_sequenceinstead require adata()function which returns a raw pointer to the start of the underlying storage. Returning aspaninstead is moderately safer, as the element count has less chance of getting lost.]Collections and ranges
For safety, collections need to be able to perform bounds checking. We can do this cheaply for sized, contiguous ranges, by using an integer index as the position type and doing
ranges::data()[idx]in the read operation -- in other words, exactly what we do in Flux today. We can also extend this to all sized random-access ranges by usingranges::begin()[idx]in thereadimplementation -- which may be slightly more expensive than using a raw pointer, but is still guaranteed to be O(1).So, going from the ranges world to the Flux world can be summarised as follows:
Ris a sized, random-access range, then it is also automatically Fluxrandom_access_collection. IfRis additionally acontiguous_rangethen it is also acontiguous_collection. This applies to things like raw arrays,std::array,vector,string,string_view,spanetc, as well as various adapted views.Ris a FluxiterableonlyFor node-based data structures like
std::listorstd::unordered_map, there is no simple way for us to enumerate elements or perform bounds checking, so they will beiterableonly. (But it's worth remembering that Flux'siterableis isomorphic to Rust'siterator, which people seem broadly satisfied with -- there is still a lot of functionality available even withoutcollectionconformance.)Going in the other direction, things are much simpler for
collections than foriterables: every Fluxcollectioncan be (at least) aforward_range, without any need foras_rangeor pseudoiterators or anything like that. Just as we do today formultipass_sequences, we can define acollection_iterator:...along with about a billion more conditional operator overloads for bidir and random-access iterator support. We can then provide every
collectionimplementation withbegin()andend()members returningcollection_iterator(just as we do today with the almost-identicalsequence_iterator).A special note about
filterWhen we use a filtering adaptor -- in Flux, ranges, or any similar library -- the result of the filter predicate determines which elements are part of the output sequence.
That sounds obvious, but it has an important consequence. It means that mutating elements in a way that changes the result of filter predicate is equivalent to adding or removing elements from the resulting sequence. Doing so while we're iterating over the sequence is really asking for trouble, in the same way as adding or removing elements of a vector while iterating over it.
Now as it turns out, if we're only doing a single pass over the data -- or more precisely, if we only evaluate the predicate once per element -- then we can get away with arbitrary mutation, because it never actually gets observed. But if we're doing multiple passes, then changing membership of the filtered sequence "on the fly" is very very likely to cause problems. Again, this is true whether we're talking about C++ ranges, Flux, D ranges, or any other library (yes, even Rust).
This would be fine, except that mutating elements while filtering is something that people like to do fairly often. With Flux, fortunately, the worst that will happen is usually a failed bounds check at run time. But it would still be better to prevent people from getting into difficulty in the first place, if we can.
For this reason, I'm proposing that the new
flux::filterwill not be acollection: that is, it will be single-pass only. Thanks to our new iteration model, filter can still bereverse_iterable, so I'm hopeful that most of what people want to do can still be achieved even without multi-pass.For the remaining cases, I propose to add a second adaptor, tentatively called
flux::filter_collection. As the name suggests, this will modelcollection(orbidirectional_collectionif the base allows), but importantly will yield "constified" elements. This doesn't completely prevent people from mutating elements while iterating -- there's alwaysconst_cast! -- but it will hopefully serve as a deterrent.Finally, let's talk about caching. Ranges has the requirement that
begin()andend()are constant-time, and Flux will have the same requirement forbegin_pos()andend_pos()on collections. In order to satisfy this requirement, ranges'filter_viewcaches the first element which matches the filter, so that the first call tobegin()is O(N), but subsequent calls are O(1). Because of this,filter_viewdoes not support const iteration.Given the definition of our new
collectionprotocol above, we're in a tricky situation when it comes to implementingfilter_collection. We require thatbegin_pos()is callable on a const instance of a collection, while also requiring that it returns an answer in constant time. If we wanted to use caching in order to satisfy the second requirement (at least at the second time of asking), then the only solution would be amutablemember variable, which would in turn necessitate a mutex (or similar mechanism) to achieve thread safety -- definitely not something you want in what is intended to be a high-performance library.My proposed solution to this conundrum is to simply not do any caching at all in
filter_collection, and instead slap a big fat warning in the documentation that it might not meet the complexity requirements -- which is exactly what Swift does, in fact.I feel fairly confident that this is the right trade off, in part because the current
flux::filterdoesn't do any caching either, and I'm pretty sure no-one has ever noticed. In the new Flux, usingfilter_collectionwill probably be quite rare, and using in specific ways that would trigger quadratic behaviour should hopefully be very, very rare indeed.In conclusion
This has been quite a mammoth update, so thank you for making it to the end!
I'm pretty excited about the upcoming changes, and I believe Flux will become even more awesome than it already is once everything is finished. But I'm also very keen to gather feedback from the community. Does all this sound good? Are there improvements I should incorporate? Am I barking completely up the wrong tree? Please let me know below.
Meanwhile, I've started implementing these changes on the devel branch on Github. Right now there isn't an awful lot to see but if you're interested in following the latest developments, that's the place to do it.
-- Tristan
Beta Was this translation helpful? Give feedback.
All reactions