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

Adds some examples of using Reflex.Host.Class #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 30 additions & 0 deletions host-examples/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions host-examples/README.md
Original file line number Diff line number Diff line change
@@ -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.

2 changes: 2 additions & 0 deletions host-examples/Setup.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Distribution.Simple
main = defaultMain
14 changes: 14 additions & 0 deletions host-examples/default.nix
Original file line number Diff line number Diff line change
@@ -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;
}
26 changes: 26 additions & 0 deletions host-examples/host-examples.cabal
Original file line number Diff line number Diff line change
@@ -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
146 changes: 146 additions & 0 deletions host-examples/src/Host1.hs
Original file line number Diff line number Diff line change
@@ -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
141 changes: 141 additions & 0 deletions host-examples/src/Host2.hs
Original file line number Diff line number Diff line change
@@ -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
Loading