Reflex with Backpack
This package provides an implementation of Reflex specialized to Spider using Backpack, demonstrating how Backpack can be built on top of an existing, type-class based library without requiring any source modifications.
Have a GHC HEAD and cabal-install HEAD nightly installed.
Make sure the submodules are checked out (
git submodule init && git submodule update); we need unreleased versions of these packages because the released versions are not yet compatible with GHC HEAD.
cabal new-build -w /path/to/ghc-head
There are a few ways we could go about Backpack'ing Reflex, and this particular iteration has two goals:
Use the existing Reflex library without modification. It should only be necessary to copy code when we want it to be specialized; any code which is not parametric over the Reflex backend should be reused from Reflex.
Remove the performance tax of having a Reflex typeclass. All invocations of methods from Reflex should go directly to the actual implementation; we should be as fast as the "specialize Reflex to Spider" mode that the regular Reflex library can be run as.
The general idea is to replace the
classes with Backpack signatures representing them,
Reflex.Host.Sig (respectively). We have two internal libraries:
indef, which represents code that only depends on the
host, which represnets code that also depends on
ReflexHost (we split them because the
Pure Reflex implementation
does not support
Let's take a side-by-side look at the
Reflex type class,
and the corresponding
-- Type class class ( MonadHold t (PushM t) , MonadSample t (PullM t) , MonadFix (PushM t) , Functor (Dynamic t) , Applicative (Dynamic t) , Monad (Dynamic t) ) => Reflex t where data Behavior t :: * -> * data Event t :: * -> * data Dynamic t :: * -> * data Incremental t :: * -> * type PushM t :: * -> * type PullM t :: * -> * never :: Event t a ... -- Signature class HasTimeline t data Impl t type Event t = C.Event (Impl t) type Dynamic t = C.Dynamic (Impl t) type Behavior t = C.Behavior (Impl t) type Incremental t = C.Incremental (Impl t) type EventSelector t = C.EventSelector (Impl t) data PushM t a data PullM t a instance HasTimeline t => Functor (Dynamic t) instance HasTimeline t => Applicative (Dynamic t) instance HasTimeline t => Monad (Dynamic t) instance HasTimeline t => MonadSample (Impl t) (PullM t) instance HasTimeline t => MonadHold (Impl t) (PushM t) instance HasTimeline t => MonadSample (Impl t) (PushM t) instance HasTimeline t => MonadFix (PushM t) instance HasTimeline t => Monad (PushM t) instance HasTimeline t => Applicative (PushM t) instance HasTimeline t => Functor (PushM t) instance HasTimeline t => Monad (PullM t) instance HasTimeline t => Applicative (PullM t) instance HasTimeline t => Functor (PullM t) never :: HasTimeline t => Event t a ...
Reflex type class is quite complicated, so let's step through
all of its features:
Reflexis a type class over a type
t. Unusually, this type is usually instantiated with a type constructor applied to a type variable; for example,
t, confusingly) or
SpiderTimeline x. In
Pure, the type parameter identifies a moment in time (
Intis a fairly common instantiation);
SpiderTimelineis a phantom type parameter like the
ST s awhich is used to distinguish between distinct timelines (each running instance of Spider has a timeline that cannot be intermixed with other instances.) In
trequires the instances
Enum t, HasTrie t, Ord t; in
Reflexhas a number of associated data (
Incremental), as well as two associated types (
PullM). These data and types are required to fulfill certain instances, listed in the superclasses of the instance.
Reflexhas a large number of methods that must be implemented by any Reflex engine.
We have a number of options for how to go about converting this into Backpack, but in the end I went with the following scheme:
First, we need a type to represent the main type parameter
Reflexclass. Originally, I picked a plain
data Timelineto represent this, but unfortunately, this does not work with
SpiderTimeline: if we have
pull :: PullM (SpiderTimeline x) a -> Behavior (SpiderTimeline x) a, we can't rewrite this as
pull :: PullM a -> Behavior a, as we've now lost the relationship between the phantom type parameters of
Thus, instead, we define a type constructor
data Impl t, which takes as input the extra type parameter to let us pass along the phantom type variable. We also need an abstract class
HasTimeline, which will be used to record the extra constraints on the type variable.
You might be wondering, why bother with
Implat all? Inside the modules that import the signature, we are going to want to define instances like
instance HasTimeline t => Applicative (Behavior (Impl t))If we don't have
Implhere, the instances defined by the main library (
instance Reflex t => Applicative (Behavior t)) will incoherently overlap; but we really want GHC to pick the specialized instances.
Next, we need types for all of the associated data/types of
Reflex. Here, I made an unusual design decision: for data families, rather than specify them as abstract data types in the signature, I instead refer directly to the data families from
Reflex.Class. The benefit of setting things up this way is that we can reuse any module from
reflexwhich doesn't have a
Reflexconstraint but does rely on one of these associated types; if we make new abstract types, those modules have to be re-specialized (since we would not know that
Reflex.Class.Eventis the same as
To avoid having to specify
Impleverywhere, we define type signatures for these classes which insert the
I would have liked to do this trick for the type families as well, but it doesn't work: we need to specify instances for each of the superclasses of the
Reflexclass, but type synonym families are not allowed in instance heads. There might be some relaxation to GHC's rules here which would make this possible, but at the moment, it doesn't work.
Note that we need to specify an instance for every superclass (this is how Backpack signatures behave), unlike superclasses which don't need to.
Finally, we have the actual methods from the class, which are now top-level functions. These are the functions that we are removing indirection for!