Skip to content

Commit

Permalink
Merge pull request #102 from JordanMartinez/development
Browse files Browse the repository at this point in the history
Make next minor release: PS-0.12.x-v0.9.1
  • Loading branch information
JordanMartinez committed Oct 12, 2018
2 parents 29f0d13 + 0804f9a commit 7554645
Show file tree
Hide file tree
Showing 23 changed files with 547 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**/.psa*
/**/output/
/**/.pulp-cache/
/**/dist/
/**/dist/**/*.js
/**/node_modules/
/**/package-lock.json

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ instance f :: Functor Box where
map f (Box a) = Box (f a)
```

One could also see `map` as "lifting" a function into a context, our Box-like type:
```purescript
map :: forall a b. (a -> b) -> (Box a -> Box b)
map f = (\(Box a) -> Box (f b))
```

## Laws

### Identity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
See its docs: [Applicative](https://pursuit.purescript.org/packages/purescript-prelude/4.1.0/docs/Control.Applicative)

```purescript
class (Functor f) <= Apply f where
apply :: forall a b. f (a -> b) -> f a -> f b
class (Apply f) <= Applicative f where
pure :: forall a. a -> f a
data Box a = Box a
Expand Down
13 changes: 9 additions & 4 deletions 21-Hello-World/09-Games/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Let's tie all of these ideas together. This folder is a project for a simple wor
- testing
- benchmarking (pretty sure I can't implement this yet either)
- data validation
- user input via a terminal
- user input via a terminal and via a browser-based UI

It will also demonstrate how one can write a program using a domain-driven design / Onion Architecture mentioned previously.

Expand All @@ -23,20 +23,25 @@ Start with `src` and then look at `test`.

## Compilation Instructions

To run the programs/test in this folder, copy and paste this into your terminal:
To run the programs/test in this folder, copy and paste this into your terminal. To run the program in a browser, run the corresponding command below and then open the `dist/game-name/.../index.html` file:
```bash
# The Node Readline & Aff folder
pulp --psc-package run -m ConsoleLessons.ReadLine.Effect
pulp --psc-package run -m ConsoleLessons.ReadLine.AffMonad

# The Random Number folder
## Node-Based implementation
pulp --psc-package run -m Games.RandomNumber.Free.Infrastructure
pulp --psc-package run -m Games.RandomNumber.Run.Infrastructure

# Changes in Run folder
### Changes in Run folder
pulp --psc-package run -m Games.RandomNumber.Run.ChangeImplementation
pulp --psc-package run -m Games.RandomNumber.Run.AddDomainTerm

# Run-based Test
## Browser-based implementation
pulp --psc-package browserify -O -m Games.RandomNumber.Free.Halogen.Infrastructure --to dist/random-number/free/app.js
pulp --psc-package browserify -O -m Games.RandomNumber.Run.Halogen.Infrastructure --to dist/random-number/run/app.js

## Run-based Test
pulp --psc-package test -m Test.Games.RandomNumber.Run.Infrastructure
```
10 changes: 10 additions & 0 deletions 21-Hello-World/09-Games/dist/random-number/free/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Game - Guess a Random Number (Free-based)</title>
</head>
<body>
<script src="app.js" charset="utf-8"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions 21-Hello-World/09-Games/dist/random-number/run/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Game - Guess a Random Number (Run-Based)</title>
</head>
<body>
<script src="app.js" charset="utf-8"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions 21-Hello-World/09-Games/psc-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"set": "psc-0.12.0-20180901",
"source": "https://github.com/purescript/package-sets.git",
"depends": [
"avar",
"halogen",
"quickcheck",
"integers",
"node-readline",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
module Games.RandomNumber.Free.Domain (RandomNumberOperationF(..), RandomNumberOperation, runCore) where

import Prelude
import Data.Functor (class Functor)

import Control.Monad.Free (Free, liftF, substFree)
import Games.RandomNumber.Free.Core ( Bounds, RandomInt, Guess, RemainingGuesses
, outOfGuesses, decrement, totalPossibleGuesses
, (==#), mkGameInfo
, GameResult(..), Game, GameF(..), game)
import Data.Functor (class Functor)
-- import Games.RandomNumber.Free (Game(..))
import Games.RandomNumber.Free.Core (Bounds, RandomInt, Guess, RemainingGuesses, outOfGuesses, decrement, totalPossibleGuesses, (==#), mkGameInfo, GameResult(..), Game, GameF(..), game)

-- | Defines the operations we'll need to run
-- | a Random Number Guessing game
Expand Down Expand Up @@ -76,6 +75,14 @@ runCore = substFree go
pure (reply $ mkGameInfo bounds randomInt totalGueses)
PlayGame ({ bound: b, number: n, remaining: remaining }) reply -> do
result <- gameLoop b n remaining
case result of
PlayerWins remaining -> do
log "Player won!"
log $ "Player guessed the random number with " <>
show remaining <> " try(s) remaining."
PlayerLoses randomInt -> do
log "Player lost!"
log $ "The number was: " <> show randomInt

pure (reply result)
-- EndGame gameResult next ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Node.ReadLine ( Interface
)
import Node.ReadLine as NR

import Games.RandomNumber.Free.Core (game, unBounds, GameResult(..))
import Games.RandomNumber.Free.Core (game, unBounds)
import Games.RandomNumber.Free.Domain (runCore)
import Games.RandomNumber.Free.API (API_F(..), API, runDomain)

Expand Down Expand Up @@ -47,17 +47,5 @@ main = do
interface <- createConsoleInterface noCompletion

runAff_
(case _ of
Left _ -> close interface
Right gameResult -> case gameResult of
PlayerWins remaining -> do
log "Player won!"
log $ "Player guessed the random number with " <>
show remaining <> " trie(s) remaining."
close interface
PlayerLoses randomInt -> do
log "Player lost!"
log $ "The number was: " <> show randomInt
close interface
)
(\_ -> close interface)
(runAPI interface (runDomain (runCore game)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
-- | This is the same code used in the Run-based version
module Games.RandomNumber.Free.Halogen.UserInput
( Language(..)
, calcLikeInput
) where

import Prelude
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Effect.Aff.Class as AC
import Effect.Aff.AVar (AVar)
import Effect.Aff.AVar as AVar
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

data Language a
= Add String a -- adds a number to the input
| Clear a -- clears out the input
| Submit a -- "submits" the input to the parent

type CalcState = { input :: String -- the curren tinput
, avar :: AVar String
}
type Msg_UserInput = String

-- | When rendering, the parent will pass in the avar
-- | that it will block on until this component puts
-- | the user's input into that avar
calcLikeInput :: H.Component HH.HTML Language (AVar String) Msg_UserInput Aff
calcLikeInput =
H.component
{ initialState: (\avar -> {input: "", avar: avar})
, render
, eval
, receiver: const Nothing
}
where
-- | The interface should look similar to calculator's interface
render :: CalcState -> H.ComponentHTML Language
render state =
HH.div_
[ HH.div_ [ HH.text $ state.input ]
, HH.table_
[ HH.tbody_
[ HH.tr_ [ numberCell "1", numberCell "2", numberCell "3"]
, HH.tr_ [ numberCell "4", numberCell "5", numberCell "6"]
, HH.tr_ [ numberCell "7", numberCell "8", numberCell "9"]
, HH.tr_
[ numberCell "0"
, HH.td_ [ HH.button [HE.onClick $ HE.input_ $ Clear] [ HH.text "Clear" ] ]
, HH.td_ [ HH.button [HE.onClick $ HE.input_ $ Submit] [ HH.text "Submit" ] ]
]
]
]
]

numberCell numText =
HH.td_
[ HH.button
[HE.onClick $ HE.input_ $ Add numText]
[ HH.text numText ]
]

-- | Change the input based on the user's button clicks
-- | and submit the final input back to parent once done
eval :: Language ~> H.ComponentDSL CalcState Language Msg_UserInput Aff
eval = case _ of
Add n next -> do
H.modify_ (\state -> state { input = state.input <> n })
pure next
Clear next -> do
H.modify_ (\s -> s { input = "" })
pure next
Submit next -> do
state <- H.get
_ <- AC.liftAff $ AVar.put state.input state.avar
pure next
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module Games.RandomNumber.Free.Halogen.Terminal (terminal) where

import Prelude
import Data.Array (snoc)
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Effect.Aff.Class as AffClass
import Effect.Aff.AVar (AVar)
import Effect.Aff.AVar as AVar
import Effect.Random (randomInt)
import Games.RandomNumber.Free.Halogen.UserInput (Language, calcLikeInput)
import Games.RandomNumber.Core (unBounds)
import Games.RandomNumber.Free.API (API_F(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen (liftEffect)

-- Rather than defining our query language
-- for this component, we'll just re-use the API language

-- | Store the messages that should appear in the terminal (history).
-- | When `getInput == Nothing`, just display the terminal.
-- | When `getInput == Just avar`, display the calculator-like interface
-- | to get the user's input.
type State = { history :: Array String
, getInput :: Maybe (AVar String)
}

-- | Rather than defining a new query language here,
-- | we'll just reuse the API_F one.
type Query = API_F

-- | No need to raise any messages to listeners outside of this
-- | root component as we'll be emitting messages via AVars.
type Message = Void

-- | There's only one child, so this slot type is overkill. Oh well...
newtype Slot = Slot Int
derive newtype instance e :: Eq Slot
derive newtype instance s :: Ord Slot

-- |
terminal :: H.Component HH.HTML Query Unit Message Aff
terminal =
H.parentComponent
{ initialState: const { history: [], getInput: Nothing }
, render
, eval
, receiver: const Nothing
}
where
render :: State -> H.ParentHTML Query Language Slot Aff
render state = case state.getInput of
Just avar ->
HH.div_
[ HH.div_ $ state.history <#> \msg -> HH.div_ [HH.text msg]
, HH.slot (Slot 1) calcLikeInput avar (HE.input Log)
]
Nothing ->
HH.div_
[ HH.div_ $ state.history <#> \msg -> HH.div_ [HH.text msg]
]

-- | Log: Our game's business logic outside this
-- | component will send a query into this root component
-- | to add the message to the terminal
-- | GetUserInput: Our game's business logic outside this
-- | component will send a query into this root component
-- | to get the user's input. This component will re-render
-- | itself with the calculator-like interface,
-- | so that user can submit their input. Evaluation will block
-- | until user submits their input. Once received, this component
-- | will re-render so that the interface disappears.
-- | and then return the user's input to the game logic code outside.
-- | GenRandomInt: We don't need to use the UI to generate a random int.
-- | However, because this instance is part of the `API_F` type,
-- | we need to account for it, so that we have a total function.
eval :: Query ~> H.ParentDSL State Query Language Slot Message Aff
eval = case _ of
Log msg next -> do
H.modify_ (\state -> state { history = state.history `snoc` msg})
pure next
GetUserInput msg reply -> do
avar <- AffClass.liftAff AVar.empty
H.modify_ (\state -> state { history = state.history `snoc` msg
, getInput = Just avar })
value <- AffClass.liftAff $ AVar.take avar
H.modify_ (\state -> state { getInput = Nothing })
pure $ reply value
GenRandomInt bounds reply -> do
random <- unBounds bounds (\l u -> liftEffect $ randomInt l u)

pure (reply random)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Games.RandomNumber.Free.Halogen.Infrastructure where

import Prelude

import Control.Monad.Free (foldFree)
import Effect (Effect)
import Effect.Aff (Aff)
import Games.RandomNumber.Free.Core (game)
import Games.RandomNumber.Free.Domain (runCore)
import Games.RandomNumber.Free.API (API_F(..), API, runDomain)
import Games.RandomNumber.Free.Halogen.Terminal (terminal)
import Halogen as H
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = do
HA.runHalogenAff do
body <- HA.awaitBody
io <- runUI terminal unit body

runAPI io.query (runDomain (runCore game))

-- | (io :: HalogenIO).query
type QueryRoot = API_F ~> Aff

runAPI :: QueryRoot -> API ~> Aff
runAPI query = foldFree query
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Halogen

As the SVG files in this folder showed, we can easily implement our game using a web browser user interface rather than a console based one.

Since I'm already somewhat familiar with it, I decided to implement this next part using Halogen. Halogen has a lot of generic/polymorphic types. So, read through my "bottom up" approach first, which introduces these types one at a time. Then, read through the "top-down" approach alongside of the flowchart:
- [My "bottom-up" explanation](https://github.com/slamdata/purescript-halogen/tree/1e13c931f242f0ea72a92ed1b560110833ab2f1c/docs/v2). I stopped at a certain point because of the currently not-well-documented API changes they are making in the upcoming `5.0.0` release.
- [Their "top-down" approach](https://github.com/slamdata/purescript-halogen/tree/v4.0.0/docs).
- [The flowchart I made](https://github.com/slamdata/purescript-halogen/issues/528#issuecomment-400071113) that helps one see how the code actually works. While this flowchart is highly accurate, the issue in which it is contained explains more context on the parts where I misunderstood something.

## Halogen Warning

**WARNING!** As of this writing, Halogen's `master` branch is currently in development and their `examples` directory within that branch has not yet been updated. If you try to compile the examples with the `master` branch checked out, it will fail to compile. Instead, check out their `v4.0.0` tag and try the examples there.

## Code Warning

The browser-based UI you will see in both the Free- and Run-based code will look utterly horrible! This is intentional for two reasons:
1. The pattern to follow is easier to see without CSS and additional events cluttering up the code base
2. While I know some HTML/CSS, my learning focus has been on Purescript. I currently don't know best practices when it comes to HTML and CSS, but I will learn those things after I get more familiar with Purescript.

0 comments on commit 7554645

Please sign in to comment.