mvc 1.0.5 → 1.1.0
raw patch · 5 files changed
+775/−777 lines, 5 filesdep ~foldlsetup-changed
Dependency ranges changed: foldl
Files
- LICENSE +24/−24
- Setup.hs +2/−2
- mvc.cabal +41/−41
- src/MVC.hs +485/−487
- src/MVC/Prelude.hs +223/−223
LICENSE view
@@ -1,24 +1,24 @@-Copyright (c) 2014 Gabriel Gonzalez -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 Gabriel Gonzalez 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. +Copyright (c) 2015 Gabriel Gonzalez+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 Gabriel Gonzalez 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.
Setup.hs view
@@ -1,2 +1,2 @@-import Distribution.Simple -main = defaultMain +import Distribution.Simple+main = defaultMain
mvc.cabal view
@@ -1,41 +1,41 @@-Name: mvc -Version: 1.0.5 -Cabal-Version: >=1.8.0.2 -Build-Type: Simple -License: BSD3 -License-File: LICENSE -Copyright: 2014 Gabriel Gonzalez -Author: Gabriel Gonzalez -Maintainer: Gabriel439@gmail.com -Bug-Reports: https://github.com/Gabriel439/Haskell-MVC-Library/issues -Synopsis: Model-view-controller -Description: Use the @mvc@ library to distill concurrent programs into pure and - single-threaded programs using the @Model@-@View@-@Controller@ pattern. This - transformation lets you: - . - * replay your program deterministically, - . - * do property-based testing of your model (like @QuickCheck@), and: - . - * equationally reason about your pure core. -Category: Control, Concurrency -Source-Repository head - Type: git - Location: https://github.com/Gabriel439/Haskell-MVC-Library - -Library - Hs-Source-Dirs: src - Build-Depends: - base >= 4 && < 5 , - async >= 2.0.0 && < 2.1, - contravariant < 1.4, - foldl < 1.1, - managed < 1.1, - mmorph >= 1.0.2 && < 1.1, - pipes >= 4.1.0 && < 4.2, - pipes-concurrency >= 2.0.3 && < 2.1, - transformers >= 0.2.0.0 && < 0.5.0.0 - Exposed-Modules: - MVC, - MVC.Prelude - GHC-Options: -O2 -Wall +Name: mvc+Version: 1.1.0+Cabal-Version: >=1.8.0.2+Build-Type: Simple+License: BSD3+License-File: LICENSE+Copyright: 2014 Gabriel Gonzalez+Author: Gabriel Gonzalez+Maintainer: Gabriel439@gmail.com+Bug-Reports: https://github.com/Gabriel439/Haskell-MVC-Library/issues+Synopsis: Model-view-controller+Description: Use the @mvc@ library to distill concurrent programs into pure and+ single-threaded programs using the @Model@-@View@-@Controller@ pattern. This+ transformation lets you:+ .+ * replay your program deterministically,+ .+ * do property-based testing of your model (like @QuickCheck@), and:+ .+ * equationally reason about your pure core.+Category: Control, Concurrency+Source-Repository head+ Type: git+ Location: https://github.com/Gabriel439/Haskell-MVC-Library++Library+ Hs-Source-Dirs: src+ Build-Depends:+ base >= 4 && < 5 ,+ async >= 2.0.0 && < 2.1,+ contravariant < 1.4,+ foldl >= 1.1 && < 1.2,+ managed < 1.1,+ mmorph >= 1.0.2 && < 1.1,+ pipes >= 4.1.0 && < 4.2,+ pipes-concurrency >= 2.0.3 && < 2.1,+ transformers >= 0.2.0.0 && < 0.5.0.0+ Exposed-Modules:+ MVC,+ MVC.Prelude+ GHC-Options: -O2 -Wall
src/MVC.hs view
@@ -1,487 +1,485 @@-{-| Use the `Model` - `View` - `Controller` pattern to separate impure inputs - and outputs from pure application logic so that you can: - - * Equationally reason about your model - - * Exercise your model with property-based testing (like @QuickCheck@) - - * Reproducibly replay your model - - The @mvc@ library uses the type system to statically enforce the separation - of impure `View`s and `Controller`s from the pure `Model`. - - Here's a small example program written using the @mvc@ library to illustrate - the core types and concepts: - -> import MVC -> import qualified MVC.Prelude as MVC -> import qualified Pipes.Prelude as Pipes -> -> external :: Managed (View String, Controller String) -> external = do -> c1 <- MVC.stdinLines -> c2 <- MVC.tick 1 -> return (MVC.stdoutLines, c1 <> fmap show c2) -> -> model :: Model () String String -> model = asPipe (Pipes.takeWhile (/= "quit")) -> -> main :: IO () -> main = runMVC () model external - - This program has three components: - - * A `Controller` that interleaves lines from standard input with periodic - ticks - - * A `View` that writes lines to standard output - - * A pure `Model`, which forwards lines until the user inputs \"quit\" - - 'runMVC' connects them into a complete program, which outputs a @()@ every - second and also echoes standard input to standard output until the user - enters \"quit\": - ->>> main -() -Test<Enter> -Test -() -() -42<Enter> -42 -() -quit<enter> ->>> - - The following sections give extended guidance for how to structure @mvc@ - programs. Additionally, there is an "MVC.Prelude" module, which provides - several utilities and provides a more elaborate code example using the - @sdl@ library. --} - -{-# LANGUAGE RankNTypes #-} - -module MVC ( - -- * Controllers - -- $controller - Controller - , asInput - , keeps - - -- * Views - -- $view - , View - , asSink - , asFold - , handles - - -- * Models - -- $model - , Model - , asPipe - - -- * MVC - -- $mvc - , runMVC - - -- * Managed resources - -- $managed - , Managed - , managed - - -- *ListT - , loop - -- $listT - - -- * Re-exports - -- $reexports - , module Data.Functor.Constant - , module Data.Functor.Contravariant - , module Data.Monoid - , module Pipes - , module Pipes.Concurrent - ) where - -import Control.Applicative (Applicative) -import Control.Category (Category(..)) -import Control.Foldl (FoldM(..), impurely, premapM, pretraverseM) -import Control.Monad.Managed (Managed, managed, with) -import Control.Monad.Morph (generalize) -import Control.Monad.Trans.State.Strict (State, execStateT) -import Data.Functor.Constant (Constant(Constant, getConstant)) -import Data.Functor.Contravariant (Contravariant(contramap)) -import Data.Monoid (Monoid(mempty, mappend, mconcat), (<>), First) -import qualified Data.Monoid as M -import Pipes -import Pipes.Concurrent -import Pipes.Prelude (foldM) - -import Prelude hiding ((.), id) - -{- $controller - `Controller`s represent concurrent inputs to your system. Use the `Functor` - and `Monoid` instances for `Controller` and `Managed` to unify multiple - `Managed` `Controller`s together into a single `Managed` `Controller`: - -> controllerA :: Managed (Controller A) -> controllerB :: Managed (Controller B) -> controllerC :: Managed (Controller C) -> -> data TotalInput = InA A | InB B | InC C -> -> controllerTotal :: Managed (Controller TotalInput) -> controllerTotal = -> fmap (fmap InA) controllerA -> <> fmap (fmap InB) controllerB -> <> fmap (fmap InC) controllerC - - Combining `Controller`s interleaves their values. --} - -{-| A concurrent source - -> fmap f (c1 <> c2) = fmap f c1 <> fmap f c2 -> -> fmap f mempty = mempty --} -newtype Controller a = AsInput (Input a) --- This is just a newtype wrapper around `Input` because: --- --- * I want the `Controller` name to "stick" in inferred types --- --- * I want to restrict the API to ensure that `runMVC` is the only way to --- consume `Controller`s. This enforces strict separation of `Controller` --- logic from `Model` or `View` logic - --- Deriving `Functor` -instance Functor Controller where - fmap f (AsInput i) = AsInput (fmap f i) - --- Deriving `Monoid` -instance Monoid (Controller a) where - mappend (AsInput i1) (AsInput i2) = AsInput (mappend i1 i2) - - mempty = AsInput mempty - --- | Create a `Controller` from an `Input` -asInput :: Input a -> Controller a -asInput = AsInput -{-# INLINABLE asInput #-} - -{-| Think of the type as one of the following types: - -> keeps :: Prism' a b -> Controller a -> Controller b -> keeps :: Traversal' a b -> Controller a -> Controller b - - @(keeps prism controller)@ only emits values if the @prism@ matches the - @controller@'s output. - -> keeps (p1 . p2) = keeps p2 . keeps p1 -> -> keeps id = id - -> keeps p (c1 <> c2) = keeps p c1 <> keeps p c2 -> -> keeps p mempty = mempty --} -keeps - :: ((b -> Constant (First b) b) -> (a -> Constant (First b) a)) - -- ^ - -> Controller a - -- ^ - -> Controller b -keeps k (AsInput (Input recv_)) = AsInput (Input recv_') - where - recv_' = do - ma <- recv_ - case ma of - Nothing -> return Nothing - Just a -> case match a of - Nothing -> recv_' - Just b -> return (Just b) - match = M.getFirst . getConstant . k (Constant . M.First . Just) -{-# INLINABLE keeps #-} - -{- $view - `View`s represent outputs of your system. Use `handles` and the `Monoid` - instance of `View` to unify multiple `View`s together into a single `View`: - -> viewD :: Managed (View D) -> viewE :: Managed (View E) -> viewF :: Managed (View F) -> -> data TotalOutput = OutD D | OutE E | OutF F -> -> makePrisms ''TotalOutput -- Generates _OutD, _OutE, and _OutF prisms -> -> viewTotal :: Managed (View TotalOutput) -> viewTotal = -> fmap (handles _OutD) viewD -> <> fmap (handles _OutE) viewE -> <> fmap (handles _OutF) viewF - - Combining `View`s sequences their outputs. - - If a @lens@ dependency is too heavy-weight, then you can manually generate - `Traversal`s, which `handles` will also accept. Here is an example of how - you can generate `Traversal`s by hand with no dependencies: - -> -- _OutD :: Traversal' TotalOutput D -> _OutD :: Applicative f => (D -> f D) -> (TotalOutput -> f TotalOutput) -> _OutD k (OutD d) = fmap OutD (k d) -> _OutD k t = pure t -> -> -- _OutE :: Traversal' TotalOutput E -> _OutE :: Applicative f => (E -> f E) -> (TotalOutput -> f TotalOutput) -> _OutE k (OutE d) = fmap OutE (k d) -> _OutE k t = pure t -> -> -- _OutF :: Traversal' TotalOutput F -> _OutF :: Applicative f => (F -> f F) -> (TotalOutput -> f TotalOutput) -> _OutF k (OutF d) = fmap OutF (k d) -> _OutF k t = pure t --} - -{-| An effectful sink - -> contramap f (v1 <> v2) = contramap f v1 <> contramap f v2 -> -> contramap f mempty = mempty --} -newtype View a = AsFold (FoldM IO a ()) - -instance Monoid (View a) where - mempty = AsFold mempty - mappend (AsFold fold1) (AsFold fold2) = AsFold (mempty fold1 fold2) - -instance Contravariant View where - contramap f (AsFold fold) = AsFold (premapM f fold) - --- | Create a `View` from a sink -asSink :: (a -> IO ()) -> View a -asSink sink = AsFold (FoldM step begin done) - where - step x a = do - sink a - return x - begin = return () - done = return -{-# INLINABLE asSink #-} - --- | Create a `View` from a `FoldM` -asFold :: FoldM IO a () -> View a -asFold = AsFold -{-# INLINABLE asFold #-} - -type Traversal' a b = forall f . Applicative f => (b -> f b) -> a -> f a - -{-| Think of the type as one of the following types: - -> handles :: Prism' a b -> View b -> View a -> handles :: Traversal' a b -> View b -> View a - - @(handles prism view)@ only runs the @view@ if the @prism@ matches the - input. - -> handles (p1 . p2) = handles p1 . handles p2 -> -> handles id = id - -> handles p (v1 <> v2) = handles p v1 <> handles p v2 -> -> handles p mempty = mempty --} -handles - :: Traversal' a b - -- ^ - -> View b - -- ^ - -> View a -handles k (AsFold fold) = AsFold (pretraverseM k fold) -{-# INLINABLE handles #-} - -{- $model - `Model`s are stateful streams and they sit in between `Controller`s and - `View`s. - - Use `State` to internally communicate within the `Model`. - - Read the \"ListT\" section which describes why you should prefer `ListT` - over `Pipe` when possible. - - Also, try to defer converting your `Pipe` to a `Model` until you call - `runMVC`, because the conversion is not reversible and `Pipe` is strictly - more featureful than `Model`. --} - -{-| A @(Model s a b)@ converts a stream of @(a)@s into a stream of @(b)@s while - interacting with a state @(s)@ --} -newtype Model s a b = AsPipe (Pipe a b (State s) ()) - -instance Category (Model s) where - (AsPipe m1) . (AsPipe m2) = AsPipe (m1 <-< m2) - - id = AsPipe cat - -{-| Create a `Model` from a `Pipe` - -> asPipe (p1 <-< p2) = asPipe p1 . asPipe p2 -> -> asPipe cat = id --} -asPipe :: Pipe a b (State s) () -> Model s a b -asPipe = AsPipe -{-# INLINABLE asPipe #-} - -{- $mvc - Connect a `Model`, `View`, and `Controller` and an initial state - together using `runMVC` to complete your application. - - `runMVC` is the only way to consume `View`s and `Controller`s. The types - forbid you from mixing `View` and `Controller` logic with your `Model` - logic. - - Note that `runMVC` only accepts one `View` and one `Controller`. This - enforces a single entry point and exit point for your `Model` so that you - can cleanly separate your `Model` logic from your `View` logic and - `Controller` logic. The way you add more `View`s and `Controller`s to your - program is by unifying them into a single `View` or `Controller` by using - their `Monoid` instances. See the \"Controllers\" and \"Views\" sections - for more details on how to do this. --} - -{-| Connect a `Model`, `View`, and `Controller` and initial state into a - complete application. --} -runMVC - :: s - -- ^ Initial state - -> Model s a b - -- ^ Program logic - -> Managed (View b, Controller a) - -- ^ Effectful output and input - -> IO s - -- ^ Returns final state -runMVC initialState (AsPipe pipe) viewController = - with viewController $ \(AsFold (FoldM step begin done), AsInput input) -> do - let step' x a = lift (step x a) - let begin' = lift begin - let done' x = lift (done x) - let fold' = FoldM step' begin' done' - flip execStateT initialState $ - impurely foldM fold' (fromInput input >-> hoist (hoist generalize) pipe) -{-# INLINABLE runMVC #-} - -{- $managed - Use `managed` to create primitive `Managed` resources and use the `Functor`, - `Applicative`, `Monad`, and `Monoid` instances for `Managed` to bundle - multiple `Managed` resources into a single `Managed` resource. - - See the source code for the \"Utilities\" section below for several examples - of how to create `Managed` resources. --} - -{-| Create a `Pipe` from a `ListT` transformation - -> loop (k1 >=> k2) = loop k1 >-> loop k2 -> -> loop return = cat --} -loop :: Monad m => (a -> ListT m b) -> Pipe a b m r -loop k = for cat (every . k) -{-# INLINABLE loop #-} - -{- $listT - `ListT` computations can be combined in more ways than `Pipe`s, so try to - program in `ListT` as much as possible and defer converting it to a `Pipe` - as late as possible using `loop`. - - You can combine `ListT` computations even if their inputs and outputs are - completely different: - -> -- Independent computations -> -> modelAToD :: A -> ListT (State S) D -> modelBToE :: B -> ListT (State S) E -> modelCToF :: C -> ListT (State s) F -> -> modelInToOut :: TotalInput -> ListT (State S) TotalOutput -> modelInToOut totalInput = case totalInput of -> InA a -> fmap OutD (modelAToD a) -> InB b -> fmap OutE (modelBToE b) -> InC c -> fmap OutF (modelCToF c) - - Sometimes you have multiple computations that handle different inputs but - the same output, in which case you don't need to unify their outputs: - -> -- Overlapping outputs -> -> modelAToOut :: A -> ListT (State S) Out -> modelBToOut :: B -> ListT (State S) Out -> modelCToOut :: C -> ListT (State S) Out -> -> modelInToOut :: TotalInput -> ListT (State S) TotalOutput -> modelInToOut totalInput = case totalInput of -> InA a -> modelAToOut a -> InB b -> modelBToOut b -> InC c -> modelCToOut c - - Other times you have multiple computations that handle the same input but - produce different outputs. You can unify their outputs using the `Monoid` - and `Functor` instances for `ListT`: - -> -- Overlapping inputs -> -> modelInToA :: TotalInput -> ListT (State S) A -> modelInToB :: TotalInput -> ListT (State S) B -> modelInToC :: TotalInput -> ListT (State S) C -> -> modelInToOut :: TotalInput -> ListT (State S) TotalOutput -> modelInToOut totalInput = -> fmap OutA (modelInToA totalInput) -> <> fmap OutB (modelInToB totalInput) -> <> fmap OutC (modelInToC totalInput) - - You can also chain `ListT` computations, feeding the output of the first - computation as the input to the next computation: - -> -- End-to-end -> -> modelInToMiddle :: TotalInput -> ListT (State S) MiddleStep -> modelMiddleToOut :: MiddleStep -> ListT (State S) TotalOutput -> -> modelInToOut :: TotalInput -> ListT (State S) TotalOutput -> modelInToOut = modelInToMiddle >=> modelMiddleToOut - - ... or you can just use @do@ notation if you prefer. - - However, the `Pipe` type is more general than `ListT` and can represent - things like termination. Therefore you should consider mixing `Pipe`s with - `ListT` when you need to take advantage of these extra features: - -> -- Mix ListT with Pipes -> -> pipe :: Pipe TotalInput TotalOutput (State S) () -> pipe = Pipes.takeWhile (not . isC)) >-> loop modelInToOut -> where -> isC (InC _) = True -> isC _ = False - - So promote your `ListT` logic to a `Pipe` when you need to take advantage of - these `Pipe`-specific features. --} - -{- $reexports - "Data.Functor.Constant" re-exports `Constant` - - "Data.Functor.Contravariant" re-exports `Contravariant` - - "Data.Monoid" re-exports `Monoid`, (`<>`), `mconcat`, and `First` (the type - only) - - "Pipes" re-exports everything - - "Pipes.Concurrent" re-exports everything --} +{-| Use the `Model` - `View` - `Controller` pattern to separate impure inputs+ and outputs from pure application logic so that you can:++ * Equationally reason about your model++ * Exercise your model with property-based testing (like @QuickCheck@)++ * Reproducibly replay your model++ The @mvc@ library uses the type system to statically enforce the separation+ of impure `View`s and `Controller`s from the pure `Model`.++ Here's a small example program written using the @mvc@ library to illustrate+ the core types and concepts:++> import MVC+> import qualified MVC.Prelude as MVC+> import qualified Pipes.Prelude as Pipes+>+> external :: Managed (View String, Controller String)+> external = do+> c1 <- MVC.stdinLines+> c2 <- MVC.tick 1+> return (MVC.stdoutLines, c1 <> fmap show c2)+>+> model :: Model () String String+> model = asPipe (Pipes.takeWhile (/= "quit"))+> +> main :: IO ()+> main = runMVC () model external++ This program has three components:++ * A `Controller` that interleaves lines from standard input with periodic+ ticks++ * A `View` that writes lines to standard output++ * A pure `Model`, which forwards lines until the user inputs \"quit\"++ 'runMVC' connects them into a complete program, which outputs a @()@ every+ second and also echoes standard input to standard output until the user+ enters \"quit\":++>>> main+()+Test<Enter>+Test+()+()+42<Enter>+42+()+quit<enter>+>>>++ The following sections give extended guidance for how to structure @mvc@+ programs. Additionally, there is an "MVC.Prelude" module, which provides+ several utilities and provides a more elaborate code example using the+ @sdl@ library.+-}++{-# LANGUAGE RankNTypes #-}++module MVC (+ -- * Controllers+ -- $controller+ Controller+ , asInput+ , keeps++ -- * Views+ -- $view+ , View+ , asSink+ , asFold+ , handles++ -- * Models+ -- $model+ , Model+ , asPipe++ -- * MVC+ -- $mvc+ , runMVC++ -- * Managed resources+ -- $managed+ , Managed+ , managed++ -- *ListT+ , loop+ -- $listT++ -- * Re-exports+ -- $reexports+ , module Data.Functor.Constant+ , module Data.Functor.Contravariant+ , module Data.Monoid+ , module Pipes+ , module Pipes.Concurrent+ ) where++import Control.Category (Category(..))+import Control.Foldl (FoldM(..), HandlerM, impurely, premapM)+import qualified Control.Foldl as Fold+import Control.Monad.Managed (Managed, managed, with)+import Control.Monad.Morph (generalize)+import Control.Monad.Trans.State.Strict (State, execStateT)+import Data.Functor.Constant (Constant(Constant, getConstant))+import Data.Functor.Contravariant (Contravariant(contramap))+import Data.Monoid (Monoid(mempty, mappend, mconcat), (<>), First)+import qualified Data.Monoid as M+import Pipes+import Pipes.Concurrent+import Pipes.Prelude (foldM)++import Prelude hiding ((.), id)++{- $controller+ `Controller`s represent concurrent inputs to your system. Use the `Functor`+ and `Monoid` instances for `Controller` and `Managed` to unify multiple+ `Managed` `Controller`s together into a single `Managed` `Controller`:++> controllerA :: Managed (Controller A)+> controllerB :: Managed (Controller B)+> controllerC :: Managed (Controller C)+>+> data TotalInput = InA A | InB B | InC C+>+> controllerTotal :: Managed (Controller TotalInput)+> controllerTotal =+> fmap (fmap InA) controllerA+> <> fmap (fmap InB) controllerB+> <> fmap (fmap InC) controllerC++ Combining `Controller`s interleaves their values.+-}++{-| A concurrent source++> fmap f (c1 <> c2) = fmap f c1 <> fmap f c2+>+> fmap f mempty = mempty+-}+newtype Controller a = AsInput (Input a)+-- This is just a newtype wrapper around `Input` because:+--+-- * I want the `Controller` name to "stick" in inferred types+--+-- * I want to restrict the API to ensure that `runMVC` is the only way to+-- consume `Controller`s. This enforces strict separation of `Controller`+-- logic from `Model` or `View` logic++-- Deriving `Functor`+instance Functor Controller where+ fmap f (AsInput i) = AsInput (fmap f i)++-- Deriving `Monoid`+instance Monoid (Controller a) where+ mappend (AsInput i1) (AsInput i2) = AsInput (mappend i1 i2)++ mempty = AsInput mempty++-- | Create a `Controller` from an `Input`+asInput :: Input a -> Controller a+asInput = AsInput+{-# INLINABLE asInput #-}++{-| Think of the type as one of the following types:++> keeps :: Prism' a b -> Controller a -> Controller b+> keeps :: Traversal' a b -> Controller a -> Controller b++ @(keeps prism controller)@ only emits values if the @prism@ matches the+ @controller@'s output.++> keeps (p1 . p2) = keeps p2 . keeps p1+>+> keeps id = id++> keeps p (c1 <> c2) = keeps p c1 <> keeps p c2+>+> keeps p mempty = mempty+-}+keeps+ :: ((b -> Constant (First b) b) -> (a -> Constant (First b) a))+ -- ^+ -> Controller a+ -- ^+ -> Controller b+keeps k (AsInput (Input recv_)) = AsInput (Input recv_')+ where+ recv_' = do+ ma <- recv_+ case ma of+ Nothing -> return Nothing+ Just a -> case match a of+ Nothing -> recv_'+ Just b -> return (Just b)+ match = M.getFirst . getConstant . k (Constant . M.First . Just)+{-# INLINABLE keeps #-}++{- $view+ `View`s represent outputs of your system. Use `handles` and the `Monoid`+ instance of `View` to unify multiple `View`s together into a single `View`:++> viewD :: Managed (View D)+> viewE :: Managed (View E)+> viewF :: Managed (View F)+>+> data TotalOutput = OutD D | OutE E | OutF F+>+> makePrisms ''TotalOutput -- Generates _OutD, _OutE, and _OutF prisms+>+> viewTotal :: Managed (View TotalOutput)+> viewTotal =+> fmap (handles _OutD) viewD+> <> fmap (handles _OutE) viewE+> <> fmap (handles _OutF) viewF++ Combining `View`s sequences their outputs.++ If a @lens@ dependency is too heavy-weight, then you can manually generate+ `Traversal`s, which `handles` will also accept. Here is an example of how+ you can generate `Traversal`s by hand with no dependencies:++> -- _OutD :: Traversal' TotalOutput D+> _OutD :: Applicative f => (D -> f D) -> (TotalOutput -> f TotalOutput)+> _OutD k (OutD d) = fmap OutD (k d)+> _OutD k t = pure t+>+> -- _OutE :: Traversal' TotalOutput E+> _OutE :: Applicative f => (E -> f E) -> (TotalOutput -> f TotalOutput)+> _OutE k (OutE d) = fmap OutE (k d)+> _OutE k t = pure t+>+> -- _OutF :: Traversal' TotalOutput F+> _OutF :: Applicative f => (F -> f F) -> (TotalOutput -> f TotalOutput)+> _OutF k (OutF d) = fmap OutF (k d)+> _OutF k t = pure t+-}++{-| An effectful sink++> contramap f (v1 <> v2) = contramap f v1 <> contramap f v2+>+> contramap f mempty = mempty+-}+newtype View a = AsFold (FoldM IO a ())++instance Monoid (View a) where+ mempty = AsFold mempty+ mappend (AsFold fold1) (AsFold fold2) = AsFold (mappend fold1 fold2)++instance Contravariant View where+ contramap f (AsFold fold) = AsFold (premapM f fold)++-- | Create a `View` from a sink+asSink :: (a -> IO ()) -> View a+asSink sink = AsFold (FoldM step begin done)+ where+ step x a = do+ sink a+ return x+ begin = return ()+ done = return+{-# INLINABLE asSink #-}++-- | Create a `View` from a `FoldM`+asFold :: FoldM IO a () -> View a+asFold = AsFold+{-# INLINABLE asFold #-}++{-| Think of the type as one of the following types:++> handles :: Prism' a b -> View b -> View a+> handles :: Traversal' a b -> View b -> View a++ @(handles prism view)@ only runs the @view@ if the @prism@ matches the+ input.++> handles (p1 . p2) = handles p1 . handles p2+>+> handles id = id++> handles p (v1 <> v2) = handles p v1 <> handles p v2+>+> handles p mempty = mempty+-}+handles+ :: HandlerM IO a b+ -- ^+ -> View b+ -- ^+ -> View a+handles k (AsFold fold) = AsFold (Fold.handlesM k fold)+{-# INLINABLE handles #-}++{- $model+ `Model`s are stateful streams and they sit in between `Controller`s and+ `View`s.++ Use `State` to internally communicate within the `Model`.++ Read the \"ListT\" section which describes why you should prefer `ListT`+ over `Pipe` when possible.++ Also, try to defer converting your `Pipe` to a `Model` until you call+ `runMVC`, because the conversion is not reversible and `Pipe` is strictly+ more featureful than `Model`.+-}++{-| A @(Model s a b)@ converts a stream of @(a)@s into a stream of @(b)@s while+ interacting with a state @(s)@+-}+newtype Model s a b = AsPipe (Pipe a b (State s) ())++instance Category (Model s) where+ (AsPipe m1) . (AsPipe m2) = AsPipe (m1 <-< m2)++ id = AsPipe cat++{-| Create a `Model` from a `Pipe`++> asPipe (p1 <-< p2) = asPipe p1 . asPipe p2+>+> asPipe cat = id+-}+asPipe :: Pipe a b (State s) () -> Model s a b+asPipe = AsPipe+{-# INLINABLE asPipe #-}++{- $mvc+ Connect a `Model`, `View`, and `Controller` and an initial state+ together using `runMVC` to complete your application.++ `runMVC` is the only way to consume `View`s and `Controller`s. The types+ forbid you from mixing `View` and `Controller` logic with your `Model`+ logic.++ Note that `runMVC` only accepts one `View` and one `Controller`. This+ enforces a single entry point and exit point for your `Model` so that you+ can cleanly separate your `Model` logic from your `View` logic and+ `Controller` logic. The way you add more `View`s and `Controller`s to your+ program is by unifying them into a single `View` or `Controller` by using+ their `Monoid` instances. See the \"Controllers\" and \"Views\" sections+ for more details on how to do this.+-}++{-| Connect a `Model`, `View`, and `Controller` and initial state into a+ complete application.+-}+runMVC+ :: s+ -- ^ Initial state+ -> Model s a b+ -- ^ Program logic+ -> Managed (View b, Controller a)+ -- ^ Effectful output and input+ -> IO s+ -- ^ Returns final state+runMVC initialState (AsPipe pipe) viewController =+ with viewController $ \(AsFold (FoldM step begin done), AsInput input) -> do+ let step' x a = lift (step x a)+ let begin' = lift begin+ let done' x = lift (done x)+ let fold' = FoldM step' begin' done'+ flip execStateT initialState $+ impurely foldM fold' (fromInput input >-> hoist (hoist generalize) pipe)+{-# INLINABLE runMVC #-}++{- $managed+ Use `managed` to create primitive `Managed` resources and use the `Functor`,+ `Applicative`, `Monad`, and `Monoid` instances for `Managed` to bundle+ multiple `Managed` resources into a single `Managed` resource.++ See the source code for the \"Utilities\" section below for several examples+ of how to create `Managed` resources.+-}++{-| Create a `Pipe` from a `ListT` transformation++> loop (k1 >=> k2) = loop k1 >-> loop k2+>+> loop return = cat+-}+loop :: Monad m => (a -> ListT m b) -> Pipe a b m r+loop k = for cat (every . k)+{-# INLINABLE loop #-}++{- $listT+ `ListT` computations can be combined in more ways than `Pipe`s, so try to+ program in `ListT` as much as possible and defer converting it to a `Pipe`+ as late as possible using `loop`.++ You can combine `ListT` computations even if their inputs and outputs are+ completely different:++> -- Independent computations+>+> modelAToD :: A -> ListT (State S) D+> modelBToE :: B -> ListT (State S) E+> modelCToF :: C -> ListT (State s) F+>+> modelInToOut :: TotalInput -> ListT (State S) TotalOutput+> modelInToOut totalInput = case totalInput of+> InA a -> fmap OutD (modelAToD a)+> InB b -> fmap OutE (modelBToE b)+> InC c -> fmap OutF (modelCToF c)++ Sometimes you have multiple computations that handle different inputs but+ the same output, in which case you don't need to unify their outputs:++> -- Overlapping outputs+>+> modelAToOut :: A -> ListT (State S) Out+> modelBToOut :: B -> ListT (State S) Out+> modelCToOut :: C -> ListT (State S) Out+>+> modelInToOut :: TotalInput -> ListT (State S) TotalOutput+> modelInToOut totalInput = case totalInput of+> InA a -> modelAToOut a+> InB b -> modelBToOut b+> InC c -> modelCToOut c++ Other times you have multiple computations that handle the same input but+ produce different outputs. You can unify their outputs using the `Monoid`+ and `Functor` instances for `ListT`:++> -- Overlapping inputs+>+> modelInToA :: TotalInput -> ListT (State S) A+> modelInToB :: TotalInput -> ListT (State S) B+> modelInToC :: TotalInput -> ListT (State S) C+>+> modelInToOut :: TotalInput -> ListT (State S) TotalOutput+> modelInToOut totalInput =+> fmap OutA (modelInToA totalInput)+> <> fmap OutB (modelInToB totalInput)+> <> fmap OutC (modelInToC totalInput)++ You can also chain `ListT` computations, feeding the output of the first+ computation as the input to the next computation:++> -- End-to-end+>+> modelInToMiddle :: TotalInput -> ListT (State S) MiddleStep+> modelMiddleToOut :: MiddleStep -> ListT (State S) TotalOutput+>+> modelInToOut :: TotalInput -> ListT (State S) TotalOutput+> modelInToOut = modelInToMiddle >=> modelMiddleToOut++ ... or you can just use @do@ notation if you prefer.++ However, the `Pipe` type is more general than `ListT` and can represent+ things like termination. Therefore you should consider mixing `Pipe`s with+ `ListT` when you need to take advantage of these extra features:++> -- Mix ListT with Pipes+>+> pipe :: Pipe TotalInput TotalOutput (State S) ()+> pipe = Pipes.takeWhile (not . isC)) >-> loop modelInToOut+> where+> isC (InC _) = True+> isC _ = False++ So promote your `ListT` logic to a `Pipe` when you need to take advantage of+ these `Pipe`-specific features.+-}++{- $reexports+ "Data.Functor.Constant" re-exports `Constant`++ "Data.Functor.Contravariant" re-exports `Contravariant`++ "Data.Monoid" re-exports `Monoid`, (`<>`), `mconcat`, and `First` (the type+ only)++ "Pipes" re-exports everything++ "Pipes.Concurrent" re-exports everything+-}
src/MVC/Prelude.hs view
@@ -1,223 +1,223 @@-{-| Simple utilities - - The \"Example\" section at the bottom of this module contains an extended - example of how to interact with the @sdl@ library using the @mvc@ library --} - -module MVC.Prelude ( - -- * Controllers - producer - , stdinLines - , inLines - , inRead - , tick - - -- * Views - , consumer - , stdoutLines - , outLines - , outShow - - -- * Handles - , inHandle - , outHandle - - -- * Example - -- $example - ) where - -import Control.Applicative (pure, (<*)) -import Control.Concurrent.Async (withAsync) -import Control.Concurrent (threadDelay) -import Data.IORef (newIORef, readIORef, writeIORef) -import MVC -import Pipes.Internal (Proxy(..), closed) -import qualified Pipes.Prelude as Pipes -import qualified System.IO as IO - -{-| Create a `Controller` from a `Producer`, using the given `Buffer` - - If you're not sure what `Buffer` to use, try `Single` --} -producer :: Buffer a -> Producer a IO () -> Managed (Controller a) -producer buffer prod = managed $ \k -> do - (o, i, seal) <- spawn' buffer - let io = do - runEffect $ prod >-> toOutput o - atomically seal - withAsync io $ \_ -> k (asInput i) <* atomically seal -{-# INLINABLE producer #-} - --- | Read lines from standard input -stdinLines :: Managed (Controller String) -stdinLines = producer (bounded 1) Pipes.stdinLn -{-# INLINABLE stdinLines #-} - --- | Read lines from a file -inLines :: FilePath -> Managed (Controller String) -inLines filePath = do - handle <- inHandle filePath - producer (bounded 1) (Pipes.fromHandle handle) -{-# INLINABLE inLines #-} - --- | 'read' values from a file, one value per line, skipping failed parses -inRead :: Read a => FilePath -> Managed (Controller a) -inRead filePath = fmap (keeps parsed) (inLines filePath) - where - parsed k str = case reads str of - [(a, "")] -> Constant (getConstant (k a)) - _ -> pure str -{-# INLINABLE inRead #-} - --- | Emit empty values spaced by a delay in seconds -tick :: Double -> Managed (Controller ()) -tick n = producer (bounded 1) $ - lift (threadDelay (truncate (n * 1000000))) >~ cat -{-# INLINABLE tick #-} - --- | Create a `View` from a `Consumer` -consumer :: Consumer a IO () -> Managed (View a) -consumer cons0 = managed $ \k -> do - mf0 <- nextRequest cons0 - ref <- newIORef mf0 - k $ asSink $ \a -> do - mf <- readIORef ref - case mf of - Nothing -> return () - Just f -> do - mf' <- nextRequest (f a) - writeIORef ref mf' - where - nextRequest :: Consumer a IO () -> IO (Maybe (a -> Consumer a IO ())) - nextRequest cons = case cons of - Request () fa -> return (Just fa) - Respond v _ -> closed v - M m -> m >>= nextRequest - Pure () -> return Nothing -{-# INLINABLE consumer #-} - --- | Write lines to standard output -stdoutLines :: View String -stdoutLines = asSink putStrLn -{-# INLINABLE stdoutLines #-} - --- | Write lines to a file -outLines :: FilePath -> Managed (View String) -outLines filePath = do - handle <- outHandle filePath - return (asSink (IO.hPutStrLn handle)) -{-# INLINABLE outLines #-} - --- | 'show' values to a file, one value per line -outShow :: Show a => FilePath -> Managed (View a) -outShow filePath = fmap (contramap show) (outLines filePath) -{- -outShow filePath = do - handle <- outHandle filePath - return (asSink (IO.hPrint handle)) --} -{-# INLINABLE outShow #-} - --- | Read from a `FilePath` using a `Managed` `IO.Handle` -inHandle :: FilePath -> Managed IO.Handle -inHandle filePath = managed (IO.withFile filePath IO.ReadMode) -{-# INLINABLE inHandle #-} - --- | Write to a `FilePath` using a `Managed` `IO.Handle` -outHandle :: FilePath -> Managed IO.Handle -outHandle filePath = managed (IO.withFile filePath IO.WriteMode) -{-# INLINABLE outHandle #-} - -{- $example - The following example distils a @sdl@-based program into pure and impure - components. This program will draw a white rectangle between every two - mouse clicks. - - The first half of the program contains all the concurrent and impure logic. - The `View` and `Controller` must be `Managed` together since they both share - the same initialization logic: - -> import Control.Monad (join) -> import Graphics.UI.SDL as SDL -> import Lens.Family.Stock (_Left, _Right) -- from `lens-family-core` -> import MVC -> import MVC.Prelude -> import qualified Pipes.Prelude as Pipes -> -> data Done = Done deriving (Eq, Show) -> -> sdl :: Managed (View (Either Rect Done), Controller Event) -> sdl = join $ managed $ \k -> withInit [InitVideo, InitEventthread] $ do -> surface <- setVideoMode 640 480 32 [SWSurface] -> white <- mapRGB (surfaceGetPixelFormat surface) 255 255 255 -> -> let done :: View Done -> done = asSink (\Done -> SDL.quit) -> -> drawRect :: View Rect -> drawRect = asSink $ \rect -> do -> _ <- fillRect surface (Just rect) white -> SDL.flip surface -> -> totalOut :: View (Either Rect Done) -> totalOut = handles _Left drawRect <> handles _Right done -> -> k $ do -> totalIn <- producer Single (lift waitEvent >~ cat) -> return (totalOut, totalIn) - - Note the `Control.Monad.join` surrounding the `managed` block. This is - because the type before `Control.Monad.join` is: - -> Managed (Managed (View (Either Rect Done), Controller Event)) - - More generally, note that `Managed` is a `Monad`, so you can use @do@ - notation to combine multiple `Managed` resources into a single `Managed` - resource. - - The second half of the program contains the pure logic. - -> pipe :: Monad m => Pipe Event (Either Rect Done) m () -> pipe = do -> Pipes.takeWhile (/= Quit) >-> (click >~ rectangle >~ Pipes.map Left) -> yield (Right Done) -> -> rectangle :: Monad m => Consumer' (Int, Int) m Rect -> rectangle = do -> (x1, y1) <- await -> (x2, y2) <- await -> let x = min x1 x2 -> y = min y1 y2 -> w = abs (x1 - x2) -> h = abs (y1 - y2) -> return (Rect x y w h) -> -> click :: Monad m => Consumer' Event m (Int, Int) -> click = do -> e <- await -> case e of -> MouseButtonDown x y ButtonLeft -> -> return (fromIntegral x, fromIntegral y) -> _ -> click -> -> main :: IO () -> main = runMVC () (asPipe pipe) sdl - - Run the program to verify that clicks create rectangles. - - The more logic you move into the pure core the more you can exercise your - program purely, either manually: - ->>> let leftClick (x, y) = MouseButtonDown x y ButtonLeft ->>> Pipes.toList (each [leftClick (10, 10), leftClick (15, 16), Quit] >-> pipe) -[Left (Rect {rectX = 10, rectY = 10, rectW = 5, rectH = 6}),Right Done] - - ... or automatically using property-based testing (such as @QuickCheck@): - ->>> import Test.QuickCheck ->>> quickCheck $ \xs -> length (Pipes.toList (each (map leftClick xs) >-> pipe)) == length xs `div` 2 -+++ OK, passed 100 tests. - - Equally important, you can formally prove properties about your model using - equational reasoning because the model is `IO`-free and concurrency-free. --} +{-| Simple utilities++ The \"Example\" section at the bottom of this module contains an extended+ example of how to interact with the @sdl@ library using the @mvc@ library+-}++module MVC.Prelude (+ -- * Controllers+ producer+ , stdinLines+ , inLines+ , inRead+ , tick++ -- * Views+ , consumer+ , stdoutLines+ , outLines+ , outShow++ -- * Handles+ , inHandle+ , outHandle++ -- * Example+ -- $example+ ) where++import Control.Applicative (pure, (<*))+import Control.Concurrent.Async (withAsync)+import Control.Concurrent (threadDelay)+import Data.IORef (newIORef, readIORef, writeIORef)+import MVC+import Pipes.Internal (Proxy(..), closed)+import qualified Pipes.Prelude as Pipes+import qualified System.IO as IO++{-| Create a `Controller` from a `Producer`, using the given `Buffer`++ If you're not sure what `Buffer` to use, try `Single`+-}+producer :: Buffer a -> Producer a IO () -> Managed (Controller a)+producer buffer prod = managed $ \k -> do+ (o, i, seal) <- spawn' buffer+ let io = do+ runEffect $ prod >-> toOutput o+ atomically seal+ withAsync io $ \_ -> k (asInput i) <* atomically seal+{-# INLINABLE producer #-}++-- | Read lines from standard input+stdinLines :: Managed (Controller String)+stdinLines = producer (bounded 1) Pipes.stdinLn+{-# INLINABLE stdinLines #-}++-- | Read lines from a file+inLines :: FilePath -> Managed (Controller String)+inLines filePath = do+ handle <- inHandle filePath+ producer (bounded 1) (Pipes.fromHandle handle)+{-# INLINABLE inLines #-}++-- | 'read' values from a file, one value per line, skipping failed parses+inRead :: Read a => FilePath -> Managed (Controller a)+inRead filePath = fmap (keeps parsed) (inLines filePath)+ where+ parsed k str = case reads str of+ [(a, "")] -> Constant (getConstant (k a))+ _ -> pure str+{-# INLINABLE inRead #-}++-- | Emit empty values spaced by a delay in seconds+tick :: Double -> Managed (Controller ())+tick n = producer (bounded 1) $+ lift (threadDelay (truncate (n * 1000000))) >~ cat+{-# INLINABLE tick #-}++-- | Create a `View` from a `Consumer`+consumer :: Consumer a IO () -> Managed (View a)+consumer cons0 = managed $ \k -> do+ mf0 <- nextRequest cons0+ ref <- newIORef mf0+ k $ asSink $ \a -> do+ mf <- readIORef ref+ case mf of+ Nothing -> return ()+ Just f -> do+ mf' <- nextRequest (f a)+ writeIORef ref mf'+ where+ nextRequest :: Consumer a IO () -> IO (Maybe (a -> Consumer a IO ()))+ nextRequest cons = case cons of+ Request () fa -> return (Just fa)+ Respond v _ -> closed v+ M m -> m >>= nextRequest+ Pure () -> return Nothing+{-# INLINABLE consumer #-}+ +-- | Write lines to standard output+stdoutLines :: View String+stdoutLines = asSink putStrLn+{-# INLINABLE stdoutLines #-}++-- | Write lines to a file+outLines :: FilePath -> Managed (View String)+outLines filePath = do+ handle <- outHandle filePath+ return (asSink (IO.hPutStrLn handle))+{-# INLINABLE outLines #-}++-- | 'show' values to a file, one value per line+outShow :: Show a => FilePath -> Managed (View a)+outShow filePath = fmap (contramap show) (outLines filePath)+{-+outShow filePath = do+ handle <- outHandle filePath+ return (asSink (IO.hPrint handle))+-}+{-# INLINABLE outShow #-}++-- | Read from a `FilePath` using a `Managed` `IO.Handle`+inHandle :: FilePath -> Managed IO.Handle+inHandle filePath = managed (IO.withFile filePath IO.ReadMode)+{-# INLINABLE inHandle #-}++-- | Write to a `FilePath` using a `Managed` `IO.Handle`+outHandle :: FilePath -> Managed IO.Handle+outHandle filePath = managed (IO.withFile filePath IO.WriteMode)+{-# INLINABLE outHandle #-}++{- $example+ The following example distils a @sdl@-based program into pure and impure+ components. This program will draw a white rectangle between every two+ mouse clicks.++ The first half of the program contains all the concurrent and impure logic.+ The `View` and `Controller` must be `Managed` together since they both share+ the same initialization logic:++> import Control.Monad (join)+> import Graphics.UI.SDL as SDL+> import Lens.Family.Stock (_Left, _Right) -- from `lens-family-core`+> import MVC+> import MVC.Prelude+> import qualified Pipes.Prelude as Pipes+> +> data Done = Done deriving (Eq, Show)+> +> sdl :: Managed (View (Either Rect Done), Controller Event)+> sdl = join $ managed $ \k -> withInit [InitVideo, InitEventthread] $ do+> surface <- setVideoMode 640 480 32 [SWSurface]+> white <- mapRGB (surfaceGetPixelFormat surface) 255 255 255+> +> let done :: View Done+> done = asSink (\Done -> SDL.quit)+> +> drawRect :: View Rect+> drawRect = asSink $ \rect -> do+> _ <- fillRect surface (Just rect) white+> SDL.flip surface+> +> totalOut :: View (Either Rect Done)+> totalOut = handles _Left drawRect <> handles _Right done+> +> k $ do+> totalIn <- producer Single (lift waitEvent >~ cat)+> return (totalOut, totalIn)++ Note the `Control.Monad.join` surrounding the `managed` block. This is+ because the type before `Control.Monad.join` is:++> Managed (Managed (View (Either Rect Done), Controller Event))++ More generally, note that `Managed` is a `Monad`, so you can use @do@+ notation to combine multiple `Managed` resources into a single `Managed`+ resource.++ The second half of the program contains the pure logic.++> pipe :: Monad m => Pipe Event (Either Rect Done) m ()+> pipe = do+> Pipes.takeWhile (/= Quit) >-> (click >~ rectangle >~ Pipes.map Left)+> yield (Right Done)+> +> rectangle :: Monad m => Consumer' (Int, Int) m Rect+> rectangle = do+> (x1, y1) <- await+> (x2, y2) <- await+> let x = min x1 x2+> y = min y1 y2+> w = abs (x1 - x2)+> h = abs (y1 - y2)+> return (Rect x y w h)+> +> click :: Monad m => Consumer' Event m (Int, Int)+> click = do+> e <- await+> case e of+> MouseButtonDown x y ButtonLeft ->+> return (fromIntegral x, fromIntegral y)+> _ -> click+> +> main :: IO ()+> main = runMVC () (asPipe pipe) sdl++ Run the program to verify that clicks create rectangles.++ The more logic you move into the pure core the more you can exercise your+ program purely, either manually:++>>> let leftClick (x, y) = MouseButtonDown x y ButtonLeft+>>> Pipes.toList (each [leftClick (10, 10), leftClick (15, 16), Quit] >-> pipe)+[Left (Rect {rectX = 10, rectY = 10, rectW = 5, rectH = 6}),Right Done]++ ... or automatically using property-based testing (such as @QuickCheck@):++>>> import Test.QuickCheck+>>> quickCheck $ \xs -> length (Pipes.toList (each (map leftClick xs) >-> pipe)) == length xs `div` 2++++ OK, passed 100 tests.++ Equally important, you can formally prove properties about your model using+ equational reasoning because the model is `IO`-free and concurrency-free.+-}