diff --git a/host-examples/LICENSE b/host-examples/LICENSE new file mode 100644 index 0000000..b4d1f2b --- /dev/null +++ b/host-examples/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2016, Dave Laing + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Dave Laing nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/host-examples/README.md b/host-examples/README.md new file mode 100644 index 0000000..1507164 --- /dev/null +++ b/host-examples/README.md @@ -0,0 +1,32 @@ + +# Reflex Host examples + +It is a bit trickier to build an event loop in `reflex` than in `reactive-banana`. +This is because the facilities provided by `reflex` are lower-level and more fine-grained. + +If you want something higher level you can use, there is [`refex-host`](https://github.com/bennofs/reflex-host). + +If you want to have a look at how to use the various low-level pieces and various things that you can do with them, this is the folder for you. + +The canonical example is [host.hs](https://github.com/reflex-frp/reflex-platform/blob/develop/examples/host.hs), and much of the work here is derived from that. + +## [Host1](./src/Host1.hs) + +The simplest example rigs up an event loop for a pure event network which has an `Event` as an input and a `Behavior` as an output. + +## [Host2](./src/Host2.hs) + +The next example rigs up an event loop for a pure event network which has an `Event` as an input and a `Event` as an output. + +## [Host3](./src/Host3.hs) + +We now change the example so that we have multiple events for both the inputs and the outputs. + +## [Host4](./src/Host4.hs) + +We add `PostBuild` here, to give us easy access to an event which fires when our event loop starts. + +## [Host5](./src/Host5.hs) + +We add `PerformEvent` here, so that we can do `IO` inside of our event network instead of bolting it on afterwards. + diff --git a/host-examples/Setup.hs b/host-examples/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/host-examples/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/host-examples/default.nix b/host-examples/default.nix new file mode 100644 index 0000000..65219d4 --- /dev/null +++ b/host-examples/default.nix @@ -0,0 +1,14 @@ +{ mkDerivation, base, dependent-map, dependent-sum, doctest, lens +, mtl, QuickCheck, ref-tf, reflex, stdenv, transformers +}: +mkDerivation { + pname = "reflex-host-examples"; + version = "0.1.0.0"; + src = ./.; + libraryHaskellDepends = [ + base dependent-map dependent-sum lens mtl ref-tf reflex + transformers + ]; + testHaskellDepends = [ base doctest QuickCheck ]; + license = stdenv.lib.licenses.bsd3; +} diff --git a/host-examples/host-examples.cabal b/host-examples/host-examples.cabal new file mode 100644 index 0000000..6aa5dd5 --- /dev/null +++ b/host-examples/host-examples.cabal @@ -0,0 +1,26 @@ +name: host-examples +version: 0.1.0.0 +license: BSD3 +license-file: LICENSE +author: Dave Laing +maintainer: dave.laing.80@gmail.com +build-type: Simple +cabal-version: >=1.10 + +library + exposed-modules: Host1 + , Host2 + , Host3 + , Host4 + , Host5 + build-depends: base >= 4.8 && < 4.10 + , mtl >= 2.2 && < 2.3 + , transformers >= 0.5 && < 0.6 + , dependent-sum >= 0.3 && < 0.4 + , dependent-map >= 0.2 && < 0.3 + , ref-tf >= 0.4 && < 0.5 + , lens >= 4.13 && < 4.15 + , reflex >= 0.5 && < 0.6 + hs-source-dirs: src + ghc-options: -Wall + default-language: Haskell2010 diff --git a/host-examples/src/Host1.hs b/host-examples/src/Host1.hs new file mode 100644 index 0000000..fd0f436 --- /dev/null +++ b/host-examples/src/Host1.hs @@ -0,0 +1,146 @@ +{- +Copyright : (c) Dave Laing, 2016 +License : BSD3 +Maintainer : dave.laing.80@gmail.com +Stability : experimental +Portability : non-portable +-} +{-# LANGUAGE RankNTypes #-} +module Host1 ( + go1 + ) where + +import Control.Monad (forever) +import Control.Monad.Fix (MonadFix) +import Control.Monad.Identity (Identity(..)) +import Control.Monad.IO.Class (liftIO) +import Data.IORef (readIORef) +import System.IO + +import Data.Dependent.Sum + +import Reflex +import Reflex.Host.Class + +-- First we define a type for our applications. +-- +-- In this case, our applications will take an +-- 'Event t String' as input return a +-- 'Behavior t Int' as output. +-- +-- While we're at it, we capture various +-- typeclass constraints that we know we're +-- going to need in this type synonym. +type SampleApp1 t m = + ( Reflex t + , MonadHold t m + , MonadFix m + ) => Event t String + -> m (Behavior t Int) + +-- This is our sample FRP application. +-- +-- It doesn't care what kind of event it gets +-- as an input, because we're just using it to +-- count the events that are occurring. +guest :: SampleApp1 t m +guest e = do + -- increment every time the input event fires + d <- foldDyn (+) 0 (1 <$ e) + -- return the running count as a behavior + return $ current d + +-- This is the code that runs our FRP applications. +host :: (forall t m. SampleApp1 t m) + -> IO () +host myGuest = + -- We use the Spider implementation of Reflex. + runSpiderHost $ do + + -- We create a new event and a trigger for the event. + (e, eTriggerRef) <- newEventWithTriggerRef + -- e :: Event t a + -- eTriggerRef :: Ref m (Maybe (EventTrigger t a)) + -- + -- This gives us an event - which we need so that + -- we can provide an input to 'myGuest' - and an event + -- trigger. + -- + -- 'Ref' is an abstraction over things like 'IORef' etc.. + -- + -- If the event isn't being used - or if it stops + -- being used due to changes in the network - the 'Ref' will + -- hold 'Nothing'. + -- + -- If something is interested in the event, then the 'Ref' + -- will hold 'Just t' where 't' is a trigger for the event. + + -- Now we set up our basic event network for use with 'myGuest e'. + b <- runHostFrame $ myGuest e + -- This will give us a 'Behavior Int' which we'll use a little later. + + -- At this point the event network is set up, but there are no + -- events firing and so nothing much is happening. + -- + -- We address that by putting together an event loop to handle + -- the firing of the event we are intersted in. + -- + -- In this case we're just going to read lines from stdin + -- and fire our event with the resulting 'String' values. + + -- First we make sure stdin is buffering things by line. + liftIO $ hSetBuffering stdin LineBuffering + -- then we start our loop: + forever $ do + -- We get a line from stdin + input <- liftIO getLine + -- and we print some debugging output, just to show that we + -- do things like that with no ill effect + liftIO $ putStrLn $ "Input Event: " ++ show input + + -- Now we read the reference holding our trigger + mETrigger <- liftIO $ readIORef eTriggerRef + case mETrigger of + -- If the value is 'Nothing', then the guest FRP network + -- doesn't care about this event at the moment, so we do nothing. + Nothing -> do + return () + -- In other host settings, where we have events that might be + -- expensive to handle from the host side, we might read the + -- reference first and then skip the expensive operation when + -- no one is listening. + + -- If there is someone listening, we get hold of the trigger and + -- use that to fire the events. + Just eTrigger -> do + -- fireEvents :: [DSum (EventTrigger t) Identity] -> m () + fireEvents [eTrigger :=> Identity input] + -- 'DSum' comes from 'dependent-sum', and allows us to deal with + -- collections of events with different types in a homogenous way, + -- but without giving up type-safety. It's really nifty, and worth + -- playing around with if you have a moment. + -- + -- At the moment we're only firing one event, so it's not that + -- exciting. + + -- There is a helper function that reads the trigger reference and fires + -- the trigger if it is not 'Nothing', so we could replace the above + -- block with: + -- fireEventRef eTriggerRef input + + -- After each time that we fire the events, we read the output + -- 'Behavior'. We do that using 'sample' - to get the current + -- value of the 'Behavior' inside of the event network - and + -- 'runHostFrame' - to cause the event network to process another + -- moment in time so that we can get hold of that value on the + -- outside of the event network. + output <- runHostFrame $ sample b + + -- We'll print our output here + liftIO $ putStrLn $ "Output Behavior: " ++ show output + +-- Now we can run our sample application ('guest') using +-- our code for hosting this kind of applications ('host'). +go1 :: IO () +go1 = + host guest diff --git a/host-examples/src/Host2.hs b/host-examples/src/Host2.hs new file mode 100644 index 0000000..0ba890f --- /dev/null +++ b/host-examples/src/Host2.hs @@ -0,0 +1,141 @@ +{- +Copyright : (c) Dave Laing, 2016 +License : BSD3 +Maintainer : dave.laing.80@gmail.com +Stability : experimental +Portability : non-portable +-} +{-# LANGUAGE RankNTypes #-} +module Host2 ( + go2 + ) where + +import Data.Maybe (isJust) +import Control.Monad (unless) +import Control.Monad.Identity (Identity(..)) +import Control.Monad.IO.Class (liftIO) +import Data.IORef (readIORef) +import System.IO + +import Data.Dependent.Sum + +import Reflex +import Reflex.Host.Class + +-- I'm going to assume that you've read through Host1.hs prior to this. + +-- We are going to update the type of our applications. +-- +-- Previously we had a 'Behavior t Int' as an output, and now we have +-- an 'Event t ()' as an output. +-- +-- In this case we're going to use that event to signal when the +-- application wants to stop, so that we can exit cleanly. +type SampleApp2 t m = + ( Reflex t + , MonadHold t m + ) => Event t String + -> m (Event t ()) + +-- This is our sample application. +-- +-- Every time our input 'Event t String' fires, we're going to check +-- to see if the 'String' value is "/quit". +-- +-- We return an event that fires when this is the case. +-- +-- It's boring for now, but we'll build on it. +guest :: SampleApp2 t m +guest e = do + let + eQuit = () <$ ffilter (== "/quit") e + return eQuit + +-- This is the code that runs our FRP applications. +host :: (forall t m. SampleApp2 t m) + -> IO () +host myGuest = + -- We use the Spider implementation of Reflex. + runSpiderHost $ do + + -- We create a new event and a trigger for the event. + (e, eTriggerRef) <- newEventWithTriggerRef + + -- We set up our basic event network to use with 'myGuest e'. + eQuit <- runHostFrame $ myGuest e + -- eQuit :: Event t () + -- This gives us an 'Event t ()' which signals the intent to quit. + + -- We want to be able to work out when that event has fired, so + -- we subscribe to the event. + hQuit <- subscribeEvent eQuit + -- hQuit :: EventHandle t () + -- + -- This gives us an event handle, which we can use to read + -- our output events. + + -- A little bit of set up: + liftIO $ hSetBuffering stdin LineBuffering + + -- We define our main loop. + -- + -- We're not using 'forever' anymore, because we want to be + -- able to exit cleanly from this loop. + + let + loop = do + -- We get a line from stdin + input <- liftIO getLine + -- and we print it out for debugging purposes + liftIO $ putStrLn $ "Input Event: " ++ show input + + -- We read the event trigger + mETrigger <- liftIO $ readIORef eTriggerRef + mQuit <- case mETrigger of + -- If no one is listening, we do nothing + Nothing -> do + return Nothing + + -- If there is someone listening, we fire our input + -- events and read from the output events. + Just eTrigger -> do + -- The firing of the events happens as usual, except: + -- fireEventsAndRead :: [DSum (EventTrigger t) Identity] -> ReadPhase m a -> m a + fireEventsAndRead [eTrigger :=> Identity input] $ do + -- we now have a read phase that happens after the events have been fired. + + -- The main thing that we do in the 'ReadPhase' is call 'readEvent' and + -- deal with its output. + + -- The event may not be occurring, so there's a 'Maybe' in there: + -- readEvent :: EventHandle t a -> m (Maybe (m a)) + mValue <- readEvent hQuit + -- and we shuffle this into a form that we can use with 'sequence': + sequence mValue + + -- Again, there is a helper functions that reads the trigger + -- reference, fires the trigger if it is not 'Nothing', and then + -- reads an output event from a particular event handle. + -- + -- The above block could be replaced with: + -- mQuit <- fireEventRefAndRead eTriggerRef input hQuit + + -- The result of this block is + -- mQuit :: Maybe () + -- which has filtered up through a few layers to get to us, but is still + -- perfectly serviceable. + + -- We print out the value for debugging purposes: + liftIO $ putStrLn $ "Output Event: " ++ show mQuit + -- and then use it to determine if we'll continue with the loop: + unless (isJust mQuit) + loop + + -- This starts the actual loop + loop + +-- Now we can run our sample application ('guest') using +-- our code for hosting this kind of applications ('host'). +go2 :: IO () +go2 = + host guest diff --git a/host-examples/src/Host3.hs b/host-examples/src/Host3.hs new file mode 100644 index 0000000..25f431d --- /dev/null +++ b/host-examples/src/Host3.hs @@ -0,0 +1,170 @@ +{- +Copyright : (c) Dave Laing, 2016 +License : BSD3 +Maintainer : dave.laing.80@gmail.com +Stability : experimental +Portability : non-portable +-} +{-# LANGUAGE RankNTypes #-} +module Host3 ( + go3 + ) where + +import Data.Maybe (isJust) +import Control.Monad (unless) +import Control.Monad.Identity (Identity(..)) +import Control.Monad.IO.Class (liftIO) +import Data.IORef (readIORef) +import System.IO + +import Data.Dependent.Sum + +import Reflex +import Reflex.Host.Class + +-- I'm going to assume that you've read through Host2.hs prior to this. + +-- We're going to introduce a more complex interface to our application in this example. + +-- We have our input events: +data Input t = Input { + -- where ieOpen fires when the application starts + ieOpen :: Event t () + -- and ieRead fires whenever the user enters a line of text + , ieRead :: Event t String + } + +-- We also have output events: +data Output t = Output { + -- where oeWrite is fired to signal that we should print a line of text to the screen + oeWrite :: Event t String + -- and ieQuit is fired to signal that we should exit the application + , oeQuit :: Event t () + } + +-- Our new application type connects these together. +type SampleApp3 t m = + ( Reflex t + , MonadHold t m + ) => Input t + -> m (Output t) + +-- This leads to our first sample application that isn't indisputably 100% boring. +guest :: SampleApp3 t m +guest (Input eOpen eRead) = do + let + -- If the user types something other than "/quit", we interpret that as a message. + eMessage = ffilter (/= "/quit") eRead + -- If the user types "/quit", we should probably exit. + eQuit = () <$ ffilter (== "/quit") eRead + + -- We'll be polite, and issue greeting and parting messages to the user. + -- Other than that we'll just be echoing their input up until they quit. + -- Perhaps it's 99% boring, but it's progress. + eWrite = leftmost [ + "Hi" <$ eOpen + , ("> " ++) <$> eMessage + , "Bye" <$ eQuit + ] + + return $ Output eWrite eQuit + +-- This is the code that runs our FRP applications. +host :: (forall t m. SampleApp3 t m) + -> IO () +host myGuest = + runSpiderHost $ do + (eOpen, eOpenTriggerRef) <- newEventWithTriggerRef + (eRead, eReadTriggerRef) <- newEventWithTriggerRef + + Output eWrite eQuit <- runHostFrame $ myGuest $ Input eOpen eRead + + hWrite <- subscribeEvent eWrite + hQuit <- subscribeEvent eQuit + + liftIO $ hSetBuffering stdin LineBuffering + + -- Everything up to here should be familiar. + + -- The rest of the code is similar to what we same in Host2.hs, but + -- this time I've refactored a bit so that I don't have to repeat + -- myself as much. + + -- The plan is that we're going to fire the 'eOpen' event, and then + -- enter into a loop where we read a line from the user, then fire + -- the 'eRead' event over and over. + -- + -- We're trying to write a general host here, so we can't assume + -- anything about the output events that we're listening to. + -- + -- It could be the case that we fire the 'eOpen' event and the + -- 'eQuit' event is fired immediately in return. We have to handle + -- anything that the deranged mind of a user might throw at us. + -- + -- For this particular host, we want to respond in the same way to + -- the events that we read after firing either of the input events. + -- To that end, we separate out the common bits. + let + -- We have a piece of code that reads from the event handles: + readPhase = do + -- This version of the host reads from both events and + -- returns both values, regardless of what they are. + mWrite <- readEvent hWrite >>= sequence + mQuit <- readEvent hQuit >>= sequence + -- If it matched what we wanted from our host, we could have + -- read from the quit event and suppressed the results of the + -- write event if the quit event had fired at the same time. + -- + -- It seems arbitrary here, but in other domains it could be + -- just what you want to prevent a write-after-close problem. + + return (mWrite, mQuit) + + -- We have a piece of code that responds to our output events: + handleOutputs mWrite mQuit = do + case mWrite of + Nothing -> return () + -- If we had a write event, print the 'String' value from + -- event: + Just w -> liftIO . putStrLn $ w + -- We can do this a little more simply with: + -- forM_ mWrite $ liftIO . putStrLn + + -- Convert the occurrence of the quit event into a 'Bool': + return $ isJust mQuit + + -- We have a piece of code to fire an event and deal with the + -- response from the output events. + fireAndProcess t v = do + mETrigger <- liftIO $ readIORef t + (mWrite, mQuit) <- case mETrigger of + Nothing -> + return (Nothing, Nothing) + Just eTrigger -> + fireEventsAndRead [eTrigger :=> Identity v] readPhase + -- This will return a 'Bool' indicating whether the quit event + -- has fired. + handleOutputs mWrite mQuit + + -- We have a piece of code that uses that to guard some kind + -- of continuation: + fireProcessAndLoop t v k = do + quit <- fireAndProcess t v + unless quit + k + + -- We use 'fireProcessAndLoop' to define our main event loop: + loop = do + input <- liftIO getLine + fireProcessAndLoop eReadTriggerRef input loop + + + -- and we also use it to fire off our open event and start the + -- event loop: + fireProcessAndLoop eOpenTriggerRef () loop + +-- Now we can run our sample application ('guest') using +-- our code for hosting this kind of applications ('host'). +go3 :: IO () +go3 = + host guest diff --git a/host-examples/src/Host4.hs b/host-examples/src/Host4.hs new file mode 100644 index 0000000..fbabe41 --- /dev/null +++ b/host-examples/src/Host4.hs @@ -0,0 +1,127 @@ +{- +Copyright : (c) Dave Laing, 2016 +License : BSD3 +Maintainer : dave.laing.80@gmail.com +Stability : experimental +Portability : non-portable +-} +{-# LANGUAGE RankNTypes #-} +module Host4 ( + go4 + ) where + +import Data.Maybe (isJust) +import Control.Monad (unless, forM_) +import Control.Monad.Identity (Identity(..)) +import Control.Monad.IO.Class (liftIO) +import Data.IORef (readIORef) +import System.IO + +import Data.Dependent.Sum + +import Reflex +import Reflex.Host.Class + +-- I'm going to assume that you've read through Host3.hs prior to this. + +-- Previously we used this: +-- +-- data Input t = Input { +-- ieOpen :: Event t () +-- , ieRead :: Event t String +-- } +-- +-- for our input events. + +-- We're going to wind our input back to 'Event t String' while keeping +-- our outputs as they were. +data Output t = Output { + oeWrite :: Event t String + , oeQuit :: Event t () + } + +-- We're doing this because Reflex already has support for an event that fires +-- after the event network has been set up. + +-- We get that support through the monad transformer 'PostBuildT': + +-- Our application type changes to: +type SampleApp4 t m = + ( Reflex t + , MonadHold t m + ) => Event t String + -> PostBuildT t m (Output t) + +-- There is a class associated with 'PostBuildT': +-- class (Reflex t, Monad m) => PostBuild t m | m -> t where +-- getPostBuild :: m (Event t ()) + +-- So we can use our concrete application type if we like: +guest :: SampleApp4 t m +-- or we can tread a little lighter and specify the constraints that need: +-- guest :: (Reflex t, MonadHold t m, MonadFix m, PostBuild t m) => Event t String -> m (Output t) +guest eRead = do + -- We use the typeclass to get hold of then post-build event whenever we need it: + eOpen <- getPostBuild + + -- The rest of this function is the same as in Host3.hs + let + eMessage = ffilter (/= "/quit") eRead + eQuit = () <$ ffilter (== "/quit") eRead + eWrite = leftmost [ + "Hi" <$ eOpen + , ("> " ++) <$> eMessage + , "Bye" <$ eQuit + ] + return $ Output eWrite eQuit + +host :: (forall t m. SampleApp4 t m) + -> IO () +host myGuest = + runSpiderHost $ do + (eOpen, eOpenTriggerRef) <- newEventWithTriggerRef + (eRead, eReadTriggerRef) <- newEventWithTriggerRef + + -- This is the only line that is different from the 'host' function in Host3.hs + Output eWrite eQuit <- runHostFrame $ runPostBuildT (myGuest eRead) eOpen + -- We pass 'runPostBuildT' a value of the appropriate type and the event we plan on + -- using to signal that the event network is ready, and then we're good to go. + + hWrite <- subscribeEvent eWrite + hQuit <- subscribeEvent eQuit + + liftIO $ hSetBuffering stdin LineBuffering + + let + readPhase = do + mWrite <- readEvent hWrite >>= sequence + mQuit <- readEvent hQuit >>= sequence + return (mWrite, mQuit) + + handleOutputs mWrite mQuit = do + forM_ mWrite $ liftIO . putStrLn + return $ isJust mQuit + + fireAndProcess t v = do + mETrigger <- liftIO $ readIORef t + (mWrite, mQuit) <- case mETrigger of + Nothing -> + return (Nothing, Nothing) + Just eTrigger -> + fireEventsAndRead [eTrigger :=> Identity v] readPhase + handleOutputs mWrite mQuit + + fireProcessAndLoop t v k = do + quit <- fireAndProcess t v + unless quit + k + + loop = do + input <- liftIO getLine + fireProcessAndLoop eReadTriggerRef input loop + + fireProcessAndLoop eOpenTriggerRef () loop + +go4 :: IO () +go4 = + host guest diff --git a/host-examples/src/Host5.hs b/host-examples/src/Host5.hs new file mode 100644 index 0000000..53fd107 --- /dev/null +++ b/host-examples/src/Host5.hs @@ -0,0 +1,229 @@ +{- +Copyright : (c) Dave Laing, 2016 +License : BSD3 +Maintainer : dave.laing.80@gmail.com +Stability : experimental +Portability : non-portable +-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE TypeFamilies #-} +module Host5 ( + go5 + ) where + +import Data.Maybe (isJust) +import Control.Monad (unless) +import Control.Monad.Identity (Identity(..)) +import Control.Monad.IO.Class (MonadIO, liftIO) +import Control.Monad.Ref +import Data.IORef (readIORef) +import System.IO + +import Data.Dependent.Sum + +import Reflex +import Reflex.Host.Class +import Reflex.PerformEvent.Base + +-- I'm going to assume that you've read through Host4.hs prior to this. + +-- Previously we were dealing with event networks which performed no IO. +-- +-- Instead, our applications used output events to signal to the 'host' function +-- that IO should be performed. We could have gotten fancy and used a free monad +-- to interpret those output events, but that would have been unnecessarily distracting. + +-- For some domains, we'll want the users to be able to perform IO from withing their +-- event networks. + +-- We can support this by bringing the 'PerformEventT' monad transformer into play. +-- (Note that above we have imported 'Reflex.PerformEvent.Base') + +-- We're going to change our application type so that we can do arbitrary IO from +-- withing the network. This will involve paring our output type to a single 'Event t ()' +-- that signals when the user wants to exit. + +-- We need to add quite a few extra constraints to our application type tot make this +-- work, but that (or using 'ConstraintKinds' to gather them together) can help to +-- make things easier for our users. + +-- Other than that, the main thing to look out for is the use of 'PerformEventT': +type SampleApp5 t m = ( Reflex t + , Ref m ~ Ref IO + , ReflexHost t + , MonadRef (HostFrame t) + , Ref (HostFrame t) ~ Ref IO + , MonadIO (HostFrame t) + ) + => Event t String + -> PostBuildT t (PerformEventT t m) (Event t ()) + +-- There is a class associated with 'PerformEventT': +-- +-- class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where +-- type Performable m :: * -> * +-- performEvent :: Event t (Performable m a) -> m (Event t a) +-- performEvent_ :: Event t (Performable m ()) -> m () + +-- For the purposes of this example, I'll stick with the concrete type rather than +-- spelling out all of the constraints or setting up the necessary constraint synonym. +guest :: SampleApp5 t m +guest eRead = do + eOpen <- getPostBuild + + let + -- We are adding a new command, so we filter messages based on a leading "/" + eMessage = ffilter ((/= "/") . take 1) eRead + -- The new command will be used to print the README.md file in the same directory + -- that this program is run from. + eCat = () <$ ffilter (== "/cat") eRead + eQuit = () <$ ffilter (== "/quit") eRead + + -- We use 'performEvent' if we care about the value returned by the IO action. + -- We will get an event that fires with the value of the IO action when it completes. + -- + -- We need to use 'liftIO' with both functions from 'PerformEvent' + eCatOut <- performEvent $ liftIO (readFile "README.md") <$ eCat + + let + eWrite = leftmost [ + "Hi" <$ eOpen + , ("> " ++) <$> eMessage + -- We fold the contents of the file into our event network for displaying strings + -- to the user. + , eCatOut + , "Bye" <$ eQuit + ] + + -- We use 'performEvent_' if we don't care about the value returned by the IO action: + performEvent_ $ (liftIO . putStrLn) <$> eWrite + + return eQuit + +host :: (forall t m. SampleApp5 t m) + -> IO () +host myGuest = + runSpiderHost $ do + (eOpen, eOpenTriggerRef) <- newEventWithTriggerRef + (eRead, eReadTriggerRef) <- newEventWithTriggerRef + + -- We _could_ use 'runPerformEventT' to deal with the 'PerformEventT' layer, and then + -- use 'runHostFrame' as before, but then we'd have to deal with the other things that + -- 'runPerformEventT' performs. + -- + -- Instead, we'll use 'hostPerformEventT', which deals with all of that for us. + -- If I can come up with a good example where I need to deviate from that, I'll add it + -- to this set of examples. + -- + -- The end result is that we'll get a tuple, containing the output of the guest + -- application and a 'FireCommand'. + (eQuit, FireCommand fire) <- hostPerformEventT $ runPostBuildT (myGuest eRead) eOpen + -- The 'FireCommand' carries a function to use instead of 'fireEventsAndRead'. + -- The new function is used to weave the IO actions into the event processing. + -- The biggest change that we see as a user is that we get a list of return values + -- from the 'fire' function (since multiple events might be happening per frame due + -- to events being triggered by 'performEvent'). + + hQuit <- subscribeEvent eQuit + + liftIO $ hSetBuffering stdin LineBuffering + + -- We need to make a handful of changes to our main loop and auxiliary functions to + -- deal with the removal of the write event and the new 'fire' function: + let + readPhase = + -- We no longer have a write event to read + readEvent hQuit >>= sequence + + handleOutputs lmQuit = do + liftIO . putStrLn $ "lmQuit: " ++ show lmQuit + -- We handle the outputs by quitting if any of our + -- results where not 'Nothing' + return $ any isJust lmQuit + + -- We need to put a type signature here so that we don't lose track of the fact that the function + -- is polymorphic in 'a'. + -- + -- We either need to be concrete about 'm' ('SpiderHost Global') and 't' ('SpiderTimeline Global'), or + -- we need to move these functions out of the 'host' function - and then the type signatures get + -- _really_ fun. + fireAndProcess :: Ref (SpiderHost Global) (Maybe (EventTrigger (SpiderTimeline Global) a)) + -> a + -> (SpiderHost Global) Bool + fireAndProcess t v = do + mETrigger <- liftIO $ readIORef t + lmQuit <- case mETrigger of + Nothing -> + -- We change our default value to reflect that + -- we are now returning a list + return [] + Just eTrigger -> + fire [eTrigger :=> Identity v] readPhase + handleOutputs lmQuit + + fireProcessAndLoop t v k = do + quit <- fireAndProcess t v + unless quit + k + + loop = do + input <- liftIO getLine + fireProcessAndLoop eReadTriggerRef input loop + + fireProcessAndLoop eOpenTriggerRef () loop + +{- +handleOutputs :: MonadIO m + => [Maybe ()] + -> m Bool +handleOutputs lmQuit = do + liftIO . putStrLn $ "lmQuit: " ++ show lmQuit + -- We handle the outputs by quitting if any of our + -- results where not 'Nothing' + return $ any isJust lmQuit + +fireAndProcess :: ( Reflex t + , MonadReflexHost t m + , MonadReadEvent t (ReadPhase m) + , MonadRef m + , Ref m ~ Ref IO + , MonadIO m + ) => FireCommand t m + -> EventHandle t () + -> Ref m (Maybe (EventTrigger t a)) + -> a + -> m Bool +fireAndProcess (FireCommand fire) hQuit t v = do + mETrigger <- liftIO $ readIORef t + lmQuit <- case mETrigger of + Nothing -> + -- We change our default value to reflect that + -- we are now returning a list + return [] + Just eTrigger -> + -- fire [eTrigger :=> Identity v] (readPhase hQuit) + fire [eTrigger :=> Identity v] (readEvent hQuit >>= sequence) + handleOutputs lmQuit + +fireProcessAndLoop :: ( Reflex t + , MonadReflexHost t m + , MonadReadEvent t (ReadPhase m) + , MonadRef m + , Ref m ~ Ref IO + , MonadIO m + ) => FireCommand t m + -> EventHandle t () + -> Ref m (Maybe (EventTrigger t a)) + -> a + -> m () + -> m () +fireProcessAndLoop fc hQuit t v k = do + quit <- fireAndProcess fc hQuit t v + unless quit + k +-} + +go5 :: IO () +go5 = + host guest