Skip to content

Commit

Permalink
Merge pull request #3 from danieljharvey/return-aff
Browse files Browse the repository at this point in the history
Return effects in Aff
  • Loading branch information
danieljharvey authored Sep 18, 2019
2 parents 4e05f59 + 9a206d1 commit 5ce2e46
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 54 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
/.psc*
/.purs*
/.psa*

/.spago/
packages.dhall
spago.dhall
60 changes: 41 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,32 +71,39 @@ data Dogs
instance hasLabelDogs :: HasLabel Dogs "dogs"

dogReducer
:: EffectfulReducer Dogs State AnyAction
dogReducer { dispatch, getState } action state
:: EffectfulReducer Dogs State LiftedAction
dogReducer { dispatch } action state
= case action of
LoadNewDog
-> do
_ <- setTimeout 20000 $ dispatch $ lift $ ApologiesThisDogIsTakingSoLong
pure $ state { dog = LookingForADog
, waiting = false
}
-> UpdateStateAndRunEffect
(state { dog = LookingForADog
, waiting = false
})
(warnAfterTimeout dispatch)

ApologiesThisDogIsTakingSoLong
-> do
currentState <- getState
case currentState.dog of
LookingForADog -> pure $ state { waiting = true }
_ -> pure state
-> case state.dog of
LookingForADog -> UpdateState $ state { waiting = true }
_ -> NoOp

GotNewDog url
-> pure $ state { dog = (FoundADog url) }
-> UpdateState $ state { dog = (FoundADog url) }

DogError _
-> pure $ state { dog = HeavenKnowsI'mMiserableNow }
-> UpdateState $ state { dog = HeavenKnowsI'mMiserableNow }

warnAfterTimeout
:: (LiftedAction -> Effect Unit)
-> Aff Unit
warnAfterTimeout dispatch =
liftEffect $ do
let action = dispatch (lift ApologiesThisDogIsTakingSoLong)
_ <- setTimeout 200 action
pure unit
```

An `EffectfulReducer` is similar to `Reducer`, but has the type signature
`(RadoxEffects state action) -> action -> state -> Effect state`.
`(RadoxEffects state action) -> action -> state -> ReducerReturn`.

`RadoxEffects` is a record containing useful functions to use in reducers, and
has the following type:
Expand All @@ -105,12 +112,29 @@ has the following type:
type RadoxEffects state action
= { dispatch :: (action -> Effect Unit)
, getState :: Effect state
, state :: state
}
```

This means that our effectful reducer is able to inspect the current state at
any point, and dispatch further actions.

What the hell is `ReducerReturn` though? If you've used Purescript Thermite or
ReasonReact this might be familiar to you - it lets us either return some new
state, some sort of effect (inside an `Aff`) or a combination of the two.

```haskell
-- | Type of return value from reducer
data ReducerReturn stateType
= NoOp
| UpdateState stateType
| UpdateStateAndRunEffect stateType (Aff Unit)
| RunEffect (Aff Unit)
```

Therefore, we use `NoOp` to do nothing, `UpdateState` to return a new `state`,
`UpdateStateAndRunEffect` to change the state and do something, or `RunEffect`
to just do something and leave the state alone.

4. We can now make a combined reducer that works on all of these at once. First we create a type that contains all our action types:

Expand All @@ -131,15 +155,13 @@ rootReducer
rootReducer dispatch state action' =
match
{ counting: \action ->
pure $ countReducer action state
UpdateState $ countReducer action state
, dogs: \action ->
dogReducer dispatch action state
} action'
```

(Our `CombinedReducer` type must return an `Effect` type - hence it adds `pure`
to the regular `Reducer` of `counting`, but does not need to for the already effectful
`EffectfulReducer` of `dogs`.)
(Our `CombinedReducer` type must return an `ReducerReturn` type from above, therefore we wrap the output of the regular `Reducer` of `counting` with `UpdateState`, but does not need to for the already wrapped `EffectfulReducer` of `dogs`.)

6. That's all quite nice - but let's actually save the outcome as well. Let's create a Radox store and use it.

Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"devDependencies": {
"purescript-psci-support": "^4.0.0",
"purescript-spec": "^3.1.0",
"purescript-spec": "^4.0.0",
"purescript-js-timers": "^4.0.1"
}
}
2 changes: 1 addition & 1 deletion src/Internal/CreateStore.purs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Radox.Internal.CreateStore where

import Prelude (bind, pure, unit, ($))
import Prelude (bind, pure, unit, ($))
import Effect (Effect)
import Effect.Ref (new)
import Data.Variant (SProxy(..), Variant, inj)
Expand Down
38 changes: 35 additions & 3 deletions src/Internal/Store.purs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Radox.Internal.Store where
module Radox.Internal.Store (update, getState) where

import Prelude (Unit, bind)
import Prelude (Unit, bind, discard, pure, unit)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Ref (Ref, read, write)
import Data.Traversable (traverse)
import Radox.Internal.Types
Expand All @@ -25,16 +26,47 @@ update stateRef listeners getState' reducers action = do
--- create effect functions for the reducers to use
let passedFuncs = { dispatch: update stateRef listeners getState' reducers
, getState: getState'
, state: oldState
}

-- calculate new state
newState <- reducers passedFuncs oldState action
let return = reducers passedFuncs oldState action
newState = stateFromResponse oldState return
aff = affFromResponse return

-- announce new state to listeners
_ <- traverse (\f -> f newState) listeners

-- save new state
write newState stateRef

-- launch side effects
launchAff_ aff

-- | calculate new state from response
stateFromResponse
:: forall stateType
. stateType
-> ReducerReturn stateType
-> stateType
stateFromResponse oldState return
= case return of
NoOp -> oldState
UpdateState state -> state
UpdateStateAndRunEffect state _ -> state
RunEffect _ -> oldState

-- | calculate which effects to fire from the response
affFromResponse
:: forall stateType
. ReducerReturn stateType
-> Aff Unit
affFromResponse return
= case return of
NoOp -> pure unit
UpdateState _ -> pure unit
UpdateStateAndRunEffect _ a -> a
RunEffect a -> a

-- | Read the current state this is saved in the mutable Ref and returns it
getState
Expand Down
33 changes: 21 additions & 12 deletions src/Internal/Types.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,40 @@ module Radox.Internal.Types where

import Prelude
import Effect (Effect)
import Effect.Aff (Aff)

type RadoxEffects combinedActionType stateType
= { dispatch :: combinedActionType -> Effect Unit
, getState :: Effect stateType
, state :: stateType
}

-- | Type of return value from reducer
data ReducerReturn stateType
= NoOp
| UpdateState stateType
| UpdateStateAndRunEffect stateType (Aff Unit)
| RunEffect (Aff Unit)

-- | Type for any user-created Reducer function that takes an Action for a specific reducer, the entire state, and returns a new copy of the state
type EffectfulReducer actionType stateType combinedActionType
= RadoxEffects combinedActionType stateType
-> actionType
-> stateType
-> Effect stateType
= RadoxEffects combinedActionType stateType ->
actionType ->
stateType ->
ReducerReturn stateType

-- | Type for a reducer that does need to trigger any side effects
type Reducer actionType stateType
= actionType
-> stateType
-> stateType
= actionType ->
stateType ->
stateType

-- | Type for the user-created Combined Reducer function, that takes a Variant of any action, and pipes it to the correct Reducer function, then returns the new state
type CombinedReducer combinedActionType stateType
= RadoxEffects combinedActionType stateType
-> stateType
-> combinedActionType
-> Effect stateType
= RadoxEffects combinedActionType stateType ->
stateType ->
combinedActionType ->
ReducerReturn stateType

-- | A Listener is a function that takes the new state and returns Effect Unit (so that it can use it to do something interesting, hopefully)
type Listeners stateType
Expand All @@ -44,4 +53,4 @@ type RadoxStore combinedActionType stateType
}

-- | Typeclass that links any given Action sum type to the label it holds in the Combined Reducer / variant
class HasLabel a (p :: Symbol) | a -> p
class HasLabel a (p :: Symbol) | a -> p
2 changes: 1 addition & 1 deletion src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module Radox (module CreateStore, module Store, module Types) where

import Radox.Internal.CreateStore (createStore, emptyStore, lift) as CreateStore
import Radox.Internal.Store (getState, update) as Store
import Radox.Internal.Types (class HasLabel, CombinedReducer, Dispatcher, EffectfulReducer, Listeners, RadoxStore, Reducer) as Types
import Radox.Internal.Types (class HasLabel, CombinedReducer, Dispatcher, EffectfulReducer, Listeners, RadoxEffects, RadoxStore, Reducer, ReducerReturn(..)) as Types
44 changes: 27 additions & 17 deletions test/Main.purs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
module Test.Main where

import Prelude (class Eq, class Show, Unit, bind, discard, pure, ($), (+), (-), (<>))
import Prelude (class Eq, class Show, Unit, bind, discard, pure, unit, ($), (+), (-), (<>))

import Data.Variant (Variant, match)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class (liftEffect)
import Effect.Timer (setTimeout)
import Test.Spec (describe, it)
import Test.Spec.Assertions (shouldEqual)
import Test.Spec.Reporter.Console (consoleReporter)
import Test.Spec.Runner (run)
import Test.Spec.Runner (runSpec)

import Radox

Expand Down Expand Up @@ -54,24 +55,33 @@ instance hasLabelDogs :: HasLabel Dogs "dogs"

dogReducer
:: EffectfulReducer Dogs State LiftedAction
dogReducer { dispatch, getState } action state
dogReducer { dispatch } action state
= case action of
LoadNewDog
-> do
_ <- setTimeout 20000 $ dispatch $ lift $ ApologiesThisDogIsTakingSoLong
pure $ state { dog = LookingForADog
, waiting = false
}
-> UpdateStateAndRunEffect (state { dog = LookingForADog
, waiting = false
})
(warnAfterTimeout dispatch)

ApologiesThisDogIsTakingSoLong
-> do
currentState <- getState
case currentState.dog of
LookingForADog -> pure $ state { waiting = true }
_ -> pure state
-> case state.dog of
LookingForADog -> UpdateState $ state { waiting = true }
_ -> NoOp

GotNewDog url
-> pure $ state { dog = (FoundADog url) }
-> UpdateState $ state { dog = (FoundADog url) }

DogError _
-> pure $ state { dog = HeavenKnowsI'mMiserableNow }
-> UpdateState $ state { dog = HeavenKnowsI'mMiserableNow }

warnAfterTimeout
:: (LiftedAction -> Effect Unit)
-> Aff Unit
warnAfterTimeout dispatch =
liftEffect $ do
let action = dispatch (lift ApologiesThisDogIsTakingSoLong)
_ <- setTimeout 200 action
pure unit

--- reducer 2

Expand Down Expand Up @@ -100,14 +110,14 @@ rootReducer
rootReducer dispatch state action' =
match
{ counting: \action ->
pure $ countReducer action state
UpdateState $ countReducer action state
, dogs: \action ->
dogReducer dispatch action state
} action'

main :: Effect Unit
main =
run [consoleReporter] do
launchAff_ $ runSpec [consoleReporter] do
describe "Radox" do
it "Increments counter twice" $ do
radox <- liftEffect $ createStore defaultState [] rootReducer
Expand Down

0 comments on commit 5ce2e46

Please sign in to comment.