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

Add Reflex.Host.Headless #413

Merged
merged 4 commits into from
May 1, 2020
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions reflex.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ library
Reflex.FastWeak,
Reflex.FunctorMaybe,
Reflex.Host.Class,
Reflex.Host.Headless,
Reflex.Network,
Reflex.NotReady.Class,
Reflex.PerformEvent.Base,
Expand Down
126 changes: 126 additions & 0 deletions src/Reflex/Host/Headless.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ConstraintKinds #-}

module Reflex.Host.Headless where

import Data.Maybe
import Reflex
import Control.Monad.Fix
import Control.Monad.Primitive
import Reflex.Host.Class
import Control.Monad.IO.Class
import Control.Monad.Ref
import Data.IORef
import Data.Dependent.Sum (DSum (..))
import Control.Concurrent.Chan (newChan, readChan)
import Control.Monad (forM, forM_)
import Control.Monad.Identity (Identity(..))

type MonadHeadlessApp t m =
( Reflex t
, MonadHold t m
, MonadFix m
, PrimMonad (HostFrame t)
, ReflexHost t
, MonadIO (HostFrame t)
, Ref m ~ IORef
, Ref (HostFrame t) ~ IORef
, MonadRef (HostFrame t)
, NotReady t m
, TriggerEvent t m
, PostBuild t m
, PerformEvent t m
, MonadIO m
, MonadIO (Performable m)
, Adjustable t m
)

-- | Run a headless FRP network. Inside the action, you will most probably use
-- the capabilities provided by the 'TriggerEvent' and 'PerformEvent' type
-- classes to interface the FRP network with the outside world. Useful for
-- testing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something that's obvious in hindsight but confused me when I was fixing reflex-basic-host: if none of your triggering events can fire, the whole thing jams up and falls over with a cryptic BlockedIndefinitelyOnMVar-style message. Worth documenting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also useful for servers, which is where I'm most excited to be playing with headless hosts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One major deficiency for servers: Spider is completely single-threaded. For anything expecting high traffic, this is likely to be a problem. However, there's no reason in principle that we couldn't have a multithreaded Reflex implementation.

runHeadlessApp
:: (forall t m. MonadHeadlessApp t m => m (Event t ()))
-- ^ The action to be run in the headless FRP network. The FRP network is
-- closed at the first occurrence of the resulting 'Event'.
-> IO ()
runHeadlessApp guest =
-- We are using the 'Spider' implementation of reflex. Running the host
-- allows us to take actions on the FRP timeline. The scoped type signature
-- specifies that our host runs on the Global timeline.
-- For more information, see 'Reflex.Spider.Internal.runSpiderHost'.
(runSpiderHost :: SpiderHost Global a -> IO a) $ do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reflex-basic-host is able to run independent networks in different threads. Here's some example code showing two basic hosts triggering each other's events. Would it be possible to do that here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I believe the answer is "yes", but is there any reason we shouldn't? I can't think of any.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great idea, but I still think it should be weighted and examined in another PR. @3noch what would be your suggestion on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a nice thing to have, but should not hold up this PR.

-- Create the "post-build" event and associated trigger. This event fires
-- once, when the application starts.
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
-- Create a queue to which we will write 'Event's that need to be
-- processed.
events <- liftIO newChan
-- Run the "guest" application, providing the appropriate context. We'll
-- return the result of the action, and a 'FireCommand' that will be used to
-- trigger events.
(result, fc@(FireCommand fire)) <- do
hostPerformEventT $ -- Allows the guest app to run
-- 'performEvent', so that actions
-- (e.g., IO actions) can be run when
-- 'Event's fire.

flip runPostBuildT postBuild $ -- Allows the guest app to access to
-- a "post-build" 'Event'

flip runTriggerEventT events $ -- Allows the guest app to create new
-- events and triggers and write
-- those triggers to a channel from
-- which they will be read and
-- processed.
guest

-- Read the trigger reference for the post-build event. This will be
-- 'Nothing' if the guest application hasn't subscribed to this event.
mPostBuildTrigger <- readRef postBuildTriggerRef

-- When there is a subscriber to the post-build event, fire the event.
forM_ mPostBuildTrigger $ \postBuildTrigger ->
fire [postBuildTrigger :=> Identity ()] $ return ()

-- Subscribe to an 'Event' of that the guest application can use to
-- request application shutdown. We'll check whether this 'Event' is firing
-- to determine whether to terminate.
shutdown <- subscribeEvent result

-- The main application loop. We wait for new events and fire those that
-- have subscribers. If we detect a shutdown request, the application
-- terminates.
fix $ \loop -> do
-- Read the next event (blocking).
ers <- liftIO $ readChan events
stop <- do
-- Fire events that have subscribers.
fireEventTriggerRefs fc ers $
-- Check if the shutdown 'Event' is firing.
readEvent shutdown >>= \case
Nothing -> return False
Just _ -> return True
if or stop
then return ()
else loop
where
-- Use the given 'FireCommand' to fire events that have subscribers
-- and call the callback for the 'TriggerInvocation' of each.
fireEventTriggerRefs
:: (MonadIO m)
=> FireCommand t m
-> [DSum (EventTriggerRef t) TriggerInvocation]
-> ReadPhase m a
-> m [a]
fireEventTriggerRefs (FireCommand fire) ers rcb = do
mes <- liftIO $
forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do
me <- readIORef er
return $ fmap (\e -> e :=> Identity a) me
a <- fire (catMaybes mes) rcb
liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb
return a