Skip to content

Commit ac573b7

Browse files
authored
add Capability Pattern recipe (#257)
* add Capability Pattern recipe * import README from source repo * comment out failing test, update README
1 parent a8997c9 commit ac573b7

File tree

14 files changed

+319
-0
lines changed

14 files changed

+319
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Running a web-compatible recipe:
9595
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/BookReactHooks/src/Main.purs)) | [BookReactHooks](recipes/BookReactHooks) | A React port of the ["HTTP - Book" Elm Example](https://elm-lang.org/examples/book). |
9696
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/ButtonsHalogenHooks/src/Main.purs)) | [ButtonsHalogenHooks](recipes/ButtonsHalogenHooks) | A Halogen port of the ["User Input - Buttons" Elm Example](https://elm-lang.org/examples/buttons). |
9797
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/ButtonsReactHooks/src/Main.purs)) | [ButtonsReactHooks](recipes/ButtonsReactHooks) | A React port of the ["User Input - Buttons" Elm Example](https://elm-lang.org/examples/buttons). |
98+
| :heavy_check_mark: | | [CapabilityPatternNode](recipes/CapabilityPatternNode) | A skeletal version of an application structuring pattern |
9899
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/CardsHalogenHooks/src/Main.purs)) | [CardsHalogenHooks](recipes/CardsHalogenHooks) | A Halogen port of the ["Random - Cards" Elm Example](https://elm-lang.org/examples/cards). |
99100
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/CardsReactHooks/src/Main.purs)) | [CardsReactHooks](recipes/CardsReactHooks) | A React port of the ["Random - Cards" Elm Example](https://elm-lang.org/examples/cards). |
100101
| | :heavy_check_mark: ([try](https://try.ps.ai/?github=JordanMartinez/purescript-cookbook/master/recipes/CatGifsHalogenHooks/src/Main.purs)) | [CatGifsHalogenHooks](recipes/CatGifsHalogenHooks) | A Halogen port of the ["HTTP - Cat GIFs" Elm Example](https://elm-lang.org/examples/cat-gifs). |
6 KB
Binary file not shown.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/bower_components/
2+
/node_modules/
3+
/.pulp-cache/
4+
/output/
5+
/generated-docs/
6+
/.psc-package/
7+
/.psc*
8+
/.purs*
9+
/.psa*
10+
/.spago
11+
/web-dist/
12+
/prod-dist/
13+
/prod/
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# CapabilityPatternNode
2+
3+
A skeletal version of an application structuring pattern
4+
5+
Expanded example of the design pattern illustrated in Jordan Martinez'
6+
[reference](https://jordanmartinez.github.io/purescript-jordans-reference-site/content/21-Hello-World/05-Application-Structure/src/02-MTL/32-The-ReaderT-Capability-Design-Pattern.html).
7+
8+
## What's in each "Layer"?
9+
10+
### Layer 4 - Types
11+
12+
Strong types & pure, total functions on those types
13+
14+
You'd hope to write as much of your code in this layer as possible but in this
15+
skeleton it's intentionally almost empty because we're concerned with the less
16+
obvious business of adapting this bit to your application, infrastructure and
17+
runtime.
18+
19+
### Layer 3 - Application
20+
21+
Effectful functions - `program` and `capabilities`
22+
23+
Called "business logic" in some descriptions of this pattern this layer
24+
contains code that essentially weaves together the concrete code from Layer 4
25+
with the abstract capabilities that can be provided _differently_ in different
26+
scenarios, such as a logging capability that maybe goes to the console in Test
27+
but goes to a Database or a socket or systemd or a logfile in development and
28+
production.
29+
30+
This layer defines:
31+
* a *program* that will run in some Monad (thus giving you freedom to run it in different Monads, see above)
32+
* all the *capabilities* that it will require of the Monad in which it runs
33+
34+
The capabilities are like "container requirements", an API to a structure in which this program is embedded
35+
36+
### Layer 2 (API) & Layer 1 (Infrastructure)
37+
38+
Together these two layers define a complete instance of one monadic container for a program
39+
These two layers need to be co-located in one file in PureScript to avoid orphan instances.
40+
41+
Together they define:
42+
* a particular Monad in which our `program` from Layer 3 can be run
43+
* a `run` function that runs the `program` in _this_ Monad
44+
* the instances for the Monad
45+
* Functor, Apply, Applicative, Bind & Monad can all be derived trivially
46+
* others that a particular Monad might need can be written explicitly
47+
* the instances that are required by the `program` in Layer 3, also will have to be written explicitly
48+
49+
There are three versions of this monadic container shown here:
50+
* *ProductionSync* - which runs in Reader & Effect
51+
* *ProductionAsync* - which runs in Reader & Aff
52+
* *Test* - which runs in only Reader
53+
54+
### Layer 0 - Main
55+
This layer is where it all comes together. A `main` is called by the underlying runtime and runs the `program` in one or another Monad.
56+
57+
## Expected Behavior:
58+
59+
The `main` runs the `program` (see linked readme) in three successive, different monad contexts: `Aff`, `Effect` and `Test`.
60+
61+
If you want to verify that a failing test would still terminate the process with an error, you can simply uncomment the second call to `Test.runApp`
62+
63+
### Node.js
64+
65+
Prints the contents of this repo's LICENSE file. Note that this recipe is run from the repo's root directory.
66+
67+
68+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Ahab
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This file just indicates that the node backend is supported.
2+
It is used for CI and autogeneration purposes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{ name = "CapabilityPatternNode"
2+
, dependencies =
3+
[ "aff"
4+
, "assert"
5+
, "console"
6+
, "effect"
7+
, "node-fs"
8+
, "node-fs-aff"
9+
, "node-readline"
10+
, "transformers"
11+
]
12+
, packages = ../../packages.dhall
13+
, sources = [ "recipes/CapabilityPatternNode/src/**/*.purs" ]
14+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module App.Application where -- Layers 4 & 3 common to Production and Test
2+
3+
import App.Types (Name, getName)
4+
import Prelude (class Monad, Unit, bind, discard, pure, ($), (<>))
5+
6+
-- | Layer 3
7+
-- | "business" logic: effectful functions
8+
9+
-- | Monads to define each capability required by the program
10+
class (Monad m) <= Logger m where
11+
log :: String -> m Unit
12+
13+
class (Monad m) <= GetUserName m where
14+
getUserName :: m Name
15+
16+
-- | a program that will run in _any_ monad that can fulfill the
17+
-- | requirements (Logger and GetUserName)
18+
program :: forall m.
19+
Logger m =>
20+
GetUserName m =>
21+
m String
22+
program = do
23+
log "what is your name?"
24+
name <- getUserName
25+
log $ "Your name is " <> getName name
26+
pure $ getName name
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module CapabilityPatternNode.Main where
2+
3+
import Prelude
4+
5+
import App.Production.Sync (runApp, Environment) as Sync
6+
import App.Production.Async (runApp, Environment) as Async
7+
import App.Test (runApp, Environment) as Test
8+
import App.Application (program)
9+
import Effect (Effect)
10+
import Effect.Aff (launchAff_)
11+
import Effect.Class (liftEffect)
12+
import Effect.Class.Console (log)
13+
import Test.Assert (assert)
14+
15+
16+
-- | Layer 0 - Running the `program` in three different contexts
17+
main :: Effect Unit
18+
main = launchAff_ do
19+
-- we can do aff-ish things here with Async/ProductionA version
20+
result <- Async.runApp program { asyncEnv: "recipes/CapabilityPatternNode/async.txt" }
21+
-- ...also able to do synchronous things (within Aff) using liftEffect
22+
liftEffect $ mainSync { productionEnv: "recipes/CapabilityPatternNode/sync.txt" }
23+
liftEffect $ mainTest { testEnv: "Test" }
24+
pure unit
25+
26+
27+
28+
-- Three different "main" functions for three different scenarios
29+
mainSync :: Sync.Environment -> Effect Unit
30+
mainSync env = do
31+
result <- Sync.runApp program env
32+
pure unit
33+
34+
mainTest :: Test.Environment -> Effect Unit
35+
mainTest env = do
36+
assert $ (Test.runApp program env) == "succeeds"
37+
log "first test succeeded, now a failing test which will crash"
38+
-- assert $ (Test.runApp program env) == "failing test"
39+
40+
mainAff1 :: Async.Environment -> Effect Unit
41+
mainAff1 env = launchAff_ do
42+
result <- Async.runApp program env
43+
pure unit
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module App.Production.Async where
2+
-- Layers One and Two have to be in same file due to orphan instance restriction
3+
4+
import Prelude
5+
6+
import App.Types (Name(..))
7+
import App.Application (class Logger, class GetUserName)
8+
import Control.Monad.Reader (class MonadAsk, ReaderT, ask, asks, runReaderT)
9+
import Effect.Aff (Aff, Milliseconds(..), delay)
10+
import Effect.Aff.Class (class MonadAff, liftAff)
11+
import Effect.Class (class MonadEffect, liftEffect)
12+
import Effect.Class.Console (log) as Console
13+
import Node.Encoding (Encoding(..))
14+
import Node.FS.Aff (readTextFile) as Async
15+
import Type.Equality (class TypeEquals, from)
16+
17+
-- | Layer 2 Define our "Production" Monad but using Aff...
18+
type Environment = { asyncEnv :: String }
19+
newtype AppMA a = AppMA (ReaderT Environment Aff a)
20+
21+
-- | ...and the means to run computations in it
22+
runApp :: forall a. AppMA a -> Environment -> Aff a
23+
runApp (AppMA reader_T) env = runReaderT reader_T env
24+
25+
-- | Layer 1 Production in Aff
26+
derive newtype instance functorAppMA :: Functor AppMA
27+
derive newtype instance applyAppMA :: Apply AppMA
28+
derive newtype instance applicativeAppMA :: Applicative AppMA
29+
derive newtype instance bindAppMA :: Bind AppMA
30+
derive newtype instance monadAppMA :: Monad AppMA
31+
derive newtype instance monadEffectAppMA :: MonadEffect AppMA
32+
derive newtype instance monadAffAppMA :: MonadAff AppMA
33+
34+
-- | Reader instance not quite as simple a derivation as "derive newtype",
35+
-- | as it needs TypeEquals for the env
36+
instance monadAskAppMA :: TypeEquals e Environment => MonadAsk e AppMA where
37+
ask = AppMA $ asks from
38+
39+
-- | implementing Logger here just to the console, but in real world you'd use
40+
-- | the available Env to determine log levels, output destination, DB handles etc
41+
-- | because this version runs in Aff you can do Aff-ish things here (not shown)
42+
instance loggerAppMA :: Logger AppMA where
43+
log = liftEffect <<< Console.log
44+
45+
-- | a version of getUserName that reads the name from a file
46+
-- | given in the Environment
47+
instance getUserNameAppMA :: GetUserName AppMA where
48+
getUserName = do
49+
env <- ask -- we still have access to underlying ReaderT
50+
liftAff do -- but we can also run computations in Aff
51+
delay $ Milliseconds 1000.0 -- 1 second
52+
contents <- Async.readTextFile UTF8 env.asyncEnv
53+
pure $ Name $ contents

0 commit comments

Comments
 (0)