reflex-collections
A re-implementation of the collection functions from Reflex and some useful additions.
This library reimplements listHoldWithKey
, listWithKey
, list
, listViewWithKey
, listWithKeyShallowDiff
and selectViewListWithKey
using some typeclass and associated type-family machinery to make these functions
more polymorphic. In addition, it adds versions which have a Maybe
in the return type to facilitiate use
with collections that have no empty state (e.g. Tree
or Array
). The originals operate only on Ord k => Data.Map.Map k
,
these will operate on Ord k => Map k
, IntMap
, (Hashable k, Ord k) => HashMap k
, []
, Data.Sequence.Seq
, and,
for listHoldWithKey
, listWithKeyShallowDiff
and the "Maybe
" versions of the rest, will also
work on Tree
(a rose tree from Data.Tree
) and (Enum k, Bounded k, Ix k) => Data.Array k
,
a sort of "Total Map", holding a value for every k
.
It should also be straightforward to add class instances for other collections so these functions will work on them directly.
There is one added collection function, listViewWithKeyShallowDiff
, which specializes listWithKeyShallowDiff
for the case when the widget returns m (Event t b)
in the same way that listViewWithKey
specializes listWithKey
for that case.
The library also contains a function to simplify handling collections of widgets which fire events that can result in changes to the collection itself,
e.g., a delete, move-up or move-down button for a list.
This is selfEditingCollection
in the module Reflex.Collections.SelfEditingCollection
.
This function requires input functions to specify how to convert the type returned by a widget event into a change of structure of the collection.
It then manages the book-keeping to update the displayed collection and the output dynamic.
Along the way, we get more polymorphic versions of Reflex.mergeMap
(Reflex.Collections.Collections.mergeOver
) and
distributeMapOverDynPure
(Reflex.Collections.Collections.distributeOverDynPure
).
mergeOver
returns Event t (KeyValueSet f a)
.
KeyValueSet f a
is some container of Key/Value pairs
which has set operations (union, difference, filter, etc.) on the keys.
KeyValueSet f a
is, for all f
so far, some sort of Map (Map
itself, or HashMap
or IntMap
).
mergeOver
cannot return Event t (f a)
since events will only fire on subsets and subsets don't make sense for all containers,
though they do for all KeyValueSets
.
For the various map-like types (Map
, HashMap
, IntMap
), KeyValueSet f a ~ f a
, so this change is invisible.
There are several typeclasses, each of which abstracts out a piece of the functionality required for the listXXX functions to operate on a collection:
There are three typeclasses representing types that Reflex can efficiently merge and sequence incrementally. You are unlikely to want to implement any of these for a new type since it requires deep support within Reflex itself, in the Patch type and Adjustable classes. If types are added there, they should be added here as well:
- A typeclass supporting efficient "merging" for the
Reflex.Event
type. That is, turning a collection of events into an event of a collection with members only for fired events:Reflex.Collections.Sequenceable.ReflexMergeable
) (with instances forDMap
andIntMap
) - A typeclass supporting efficient sequencing of Dynamics (
f (Dynamic t a) -> Dynamic t (f a)
):Reflex.Collections.Sequenceable.ReflexSequenceable
(with instances forDMap
andIntMap
) - A typeclass with a collection and patch type supporting efficient sequencing of a collection
and patch as well as reconstruction of that pair into a
Reflex.Dynamic
:Reflex.Collections.Sequenceable.PatchSequenceable
(with an instance for the pairDMap
andPatchDMap
as well the pairComposedIntMap
andComposedPatchIntMap
)
And there are three typeclasses to support mapping of collections into and out of the reflex-efficient types above:
- A class defining the key type for each collection, and implementing mapWithKey and to/from lists of key/value pairs:
Reflex.Collections.KeyedCollection.KeyedCollection
(with instances forOrd k => Map k
,Hashable k => HashMap k
,IntMap
,Tree
andIx k => Array k
). For types with instances ofData.Key.Keyed
andData.Key.FoldableWithKey
the only thing requiring implementation isfromKeyValueList
. - A class defining the Key/Value set type. NB: the "diff" type for any collection is
Diff f a ~ KeyValueSet f (Maybe a)
where aJust
value represents a new entry or an update and aNothing
value represents a delete. for a collection and providing implementations of various functions to take diffs and apply them:Reflex.Collections.Diffable.Diffable
(with instances forOrd k => Map k
,Hashable k => HashMap k
,IntMap
,Tree
and(Enum k, Bounded k, Ix k) => Array k
).
For collections with diffs that are map-like (Map
orHashMap
orIntMap
), all you need to implement are thetoDiff
andfromFullDiff
functions. - A class which implements the necessary functions to transform collections and their diffs to the types which Reflex can merge and sequence and fan efficiently:
Reflex.Collections.ToPatchType
(with instances forOrd k => Map k
,(Ord k, Hashable k) => HashMap k
,IntMap
,Tree
and(Enum k, Bounded k, Ix k) => Array k
). This class also contains functions for doing event fans on the container and its Key/Value set.
To add a new collection type, you need instances of these second three (KeyedCollection
, Diffable
and ToPatchType
) for your collection.
(TODO: An example of this in its own file).
There's a quickcheck test setup for checking that your classes are lawful
(e.g., that a KeyedCollection to and from key/value lists is isomorphic, that applyDiff (diff a b) a = b
, etc.).
So if you add a type, you can add tests to check that you are implementing things correctly.
Notes:
- Many of the original collection functions require the collection to have an empty state.
We get around this by using theWithEmpty
type (which is isomorphic to Maybe but only for things wrapped in type-constructor--WithEmpty f a
is isomorphic toMaybe (f a)
--and having special versions of the collection functions which returnDynamic t (Maybe (f a))
rather thanDynamic t (f a)
for types with no natural empty state.
We also have a combinator for simplifying the frequent case where the widget returnsm (Dynamic t (g a))
, yielding aDynamic t (Maybe (f (Dynamic t a)))
result, which can be "flattened" toDynamic t (Maybe (f a))
. - I've changed the interface for listViewWithKey slightly, returning
R.Event t (KeyValueSet f v)
instead ofR.Event t (f v)
.
For a total container, one with items for all keys, the original version doesn't make sense.
Our events will be for subsets of the keys. This doesn't change anything forMap k
sinceMap k
is its own Key/Value set.
Building: This is set up with reflex-platform (as a git submodule). So on any machine with nix:
git clone https://github.com/adamConnerSax/reflex-collections
cd reflex-collections
git submodule init
git submodule update
nix-shell -A shells.ghc
cabal new-build all
Running the demos (using the jsaddle-warp runner):
cabal new-build all
should finish with a Linking...
line. Run that executable and it should spawn a browser pointing at the demo.
If not, you may need to comment out the line in
app/Main.hs
containing spawnProcess
and then rebuild, run the exe and manually open a browser tab pointing to localhost:XXX
where XXX is the port specified in Main.hs
or Simple.hs
The "Simple" demo shows a few of the functions in action, including using selfEditingCollection
to build a re-orderable list widget.
Running the tests:
cabal new-test