Skip to content

Commit 5e0da3e

Browse files
authored
switch the archived 'yargs' library out for 'optparse' (#384)
* switch the archived 'yargs' library out for 'optparse' * remove package.json now that there are no more npm dependencies
1 parent 9b3b6b2 commit 5e0da3e

File tree

6 files changed

+81
-65
lines changed

6 files changed

+81
-65
lines changed

exercises/chapter11/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
/.purs*
99
/.psa*
1010
/.spago
11+
/index.js

exercises/chapter11/package.json

Lines changed: 0 additions & 12 deletions
This file was deleted.

exercises/chapter11/packages.dhall

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
let upstream =
2-
https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210406/packages.dhall sha256:7b6af643c2f61d936878f58b613fade6f3cb39f2b4a310f6095784c7b5285879
2+
https://github.com/purescript/package-sets/releases/download/psc-0.14.2-20210713/packages.dhall sha256:654c3148cb995f642c73b4508d987d9896e2ad3ea1d325a1e826c034c0d3cd7b
33

44
let overrides =
55
{ test-unit =

exercises/chapter11/spago.dhall

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ You can edit this file as you like.
44
-}
55
{ name = "my-project"
66
, dependencies =
7-
[ "arrays"
7+
[ "arrays"
88
, "console"
99
, "control"
1010
, "effect"
@@ -15,14 +15,14 @@ You can edit this file as you like.
1515
, "maybe"
1616
, "newtype"
1717
, "node-readline"
18+
, "optparse"
1819
, "ordered-collections"
1920
, "prelude"
2021
, "psci-support"
2122
, "strings"
2223
, "test-unit"
2324
, "transformers"
2425
, "tuples"
25-
, "yargs"
2626
]
2727
, packages = ./packages.dhall
2828
, sources = [ "src/**/*.purs", "test/**/*.purs" ]

exercises/chapter11/src/Main.purs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ module Main where
33
import Prelude
44

55
import Control.Monad.RWS (RWSResult(..), runRWS)
6-
import Data.Either (Either(..))
7-
import Data.Foldable (for_)
6+
import Data.Foldable (fold, for_)
87
import Data.GameEnvironment (GameEnvironment, gameEnvironment)
98
import Data.GameState (GameState, initialGameState)
10-
import Data.Maybe (Maybe(..))
119
import Data.Newtype (wrap)
1210
import Data.String (split)
1311
import Effect (Effect)
1412
import Effect.Console (log)
1513
import Game (game)
1614
import Node.ReadLine as RL
17-
import Node.Yargs.Applicative (Y, runY, flag, yarg)
18-
import Node.Yargs.Setup (usage)
15+
import Options.Applicative ((<**>))
16+
import Options.Applicative as OP
1917

2018
runGame :: GameEnvironment -> Effect Unit
2119
runGame env = do
@@ -38,13 +36,41 @@ runGame env = do
3836
pure unit
3937

4038
main :: Effect Unit
41-
main = runY (usage "$0 -p <player name>") $ map runGame env
39+
-- ANCHOR: main
40+
main = OP.customExecParser prefs argParser >>= runGame
41+
-- ANCHOR_END: main
4242
where
43-
env :: Y GameEnvironment
44-
env = gameEnvironment
45-
<$> yarg "p" ["player"]
46-
(Just "Player name")
47-
(Right "The player name is required")
48-
false
49-
<*> flag "d" ["debug"]
50-
(Just "Use debug mode")
43+
44+
-- ANCHOR: argParser
45+
argParser :: OP.ParserInfo GameEnvironment
46+
argParser = OP.info (env <**> OP.helper) parserOptions
47+
-- ANCHOR_END: argParser
48+
49+
-- ANCHOR: env
50+
env :: OP.Parser GameEnvironment
51+
env = gameEnvironment <$> player <*> debug
52+
53+
player :: OP.Parser String
54+
player = OP.strOption $ fold
55+
[ OP.long "player"
56+
, OP.short 'p'
57+
, OP.metavar "<player name>"
58+
, OP.help "The player's name <String>"
59+
]
60+
61+
debug :: OP.Parser Boolean
62+
debug = OP.switch $ fold
63+
[ OP.long "debug"
64+
, OP.short 'd'
65+
, OP.help "Use debug mode"
66+
]
67+
-- ANCHOR_END: env
68+
69+
prefs = OP.prefs OP.showHelpOnEmpty
70+
-- ANCHOR: parserOptions
71+
parserOptions = fold
72+
[ OP.fullDesc
73+
, OP.progDesc "Play the game as <player name>"
74+
, OP.header "Monadic Adventures! A game to learn monad transformers"
75+
]
76+
-- ANCHOR_END: parserOptions

text/chapter11.md

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@ This module's project introduces the following new dependencies:
1111
- `ordered-collections`, which provides data typs for immutable maps and sets
1212
- `transformers`, which provides implementations of standard monad transformers
1313
- `node-readline`, which provides FFI bindings to the [`readline`](https://nodejs.org/api/readline.html) interface provided by NodeJS
14-
- `yargs`, which provides an applicative interface to the [`yargs`](https://www.npmjs.com/package/yargs) command line argument processing library
15-
16-
It is also necessary to install the `yargs` module using NPM:
17-
18-
```text
19-
npm install
20-
```
14+
- `optparse`, which provides applicative parsers for processing command line arguments
2115

2216
## How To Play The Game
2317

@@ -26,20 +20,28 @@ To run the project, use `spago run`
2620
By default you will see a usage message:
2721

2822
```text
29-
node ./dist/Main.js -p <player name>
23+
Monadic Adventures! A game to learn monad transformers
3024
31-
Options:
32-
-p, --player Player name [required]
33-
-d, --debug Use debug mode
25+
Usage: run.js (-p|--player <player name>) [-d|--debug]
26+
Play the game as <player name>
3427
35-
Missing required arguments: p
36-
The player name is required
28+
Available options:
29+
-p,--player <player name>
30+
The player's name <String>
31+
-d,--debug Use debug mode
32+
-h,--help Show this help text
3733
```
3834

39-
Provide the player name using the `-p` option:
35+
To provide command line arguments, you can either call `spago run` with the `-a` option to pass additional arguments directly to your application, or you can call `spago bundle-app`, which will create an index.js file that can be run directly with `node`.
36+
For example, to provide the player name using the `-p` option:
4037

4138
```text
42-
spago run -a "-p Phil"
39+
$ spago run -a "-p Phil"
40+
>
41+
```
42+
```text
43+
$ spago bundle-app
44+
$ node index.js -p Phil
4345
>
4446
```
4547

@@ -882,7 +884,7 @@ The remainder of the `Game` module defines a set of similar actions, each using
882884

883885
Since our game logic runs in the `RWS` monad, it is necessary to run the computation in order to respond to the user's commands.
884886

885-
The front-end of our game is built using two packages: `yargs`, which provides an applicative interface to the `yargs` command line parsing library, and `node-readline`, which wraps NodeJS' `readline` module, allowing us to write interactive console-based applications.
887+
The front-end of our game is built using two packages: `optparse`, which provides applicative command line parsing, and `node-readline`, which wraps NodeJS' `readline` module, allowing us to write interactive console-based applications.
886888

887889
The interface to our game logic is provided by the function `game` in the `Game` module:
888890

@@ -972,45 +974,44 @@ The `runGame` function finally attaches the initial line handler to the console
972974

973975
## Handling Command Line Options
974976

975-
The final piece of the application is responsible for parsing command line options and creating the `GameEnvironment` configuration record. For this, we use the `yargs` package.
977+
The final piece of the application is responsible for parsing command line options and creating the `GameEnvironment` configuration record. For this, we use the `optparse` package.
976978

977-
`yargs` is an example of _applicative command line option parsing_. Recall that an applicative functor allows us to lift functions of arbitrary arity over a type constructor representing some type of side-effect. In the case of the `yargs` package, the functor we are interested in is the `Y` functor, which adds the side-effect of reading from command line options. It provides the following handler:
979+
`optparse` is an example of _applicative command line option parsing_. Recall that an applicative functor allows us to lift functions of arbitrary arity over a type constructor representing some type of side-effect. In the case of the `optparse` package, the functor we are interested in is the `Parser` functor (imported from the optparse module `Options.Applicative`, not to be confused with our `Parser` that we defined in the `Split` module), which adds the side-effect of reading from command line options. It provides the following handler:
978980

979981
```haskell
980-
runY :: forall a. YargsSetup -> Y (Effect a) -> Effect a
982+
customExecParser :: forall a. ParserPrefs ParserInfo a Effect a
981983
```
982984

983-
This is best illustrated by example. The application's `main` function is defined using `runY` as follows:
985+
This is best illustrated by example. The application's `main` function is defined using `customExecParser` as follows:
984986

985987
```haskell
986-
main = runY (usage "$0 -p <player name>") $ map runGame env
988+
{{#include ../exercises/chapter11/src/Main.purs:main}}
987989
```
988990

989-
The first argument is used to configure the `yargs` library. In our case, we simply provide a usage message, but the `Node.Yargs.Setup` module provides several other options.
991+
The first argument is used to configure the `optparse` library. In our case, we simply configure it to show the help message when the application is run without any arguments (instead of showing a "missing argument" error) by using `OP.prefs OP.showHelpOnEmpty`, but the `Options.Applicative.Builder` module provides several other options.
990992

991-
The second argument uses the `map` function to lift the `runGame` function over the `Y` type constructor. The argument `env` is constructed in a `where` declaration using the applicative operators `<$>` and `<*>`:
993+
The second argument is the complete description of our parser program:
994+
```haskell
995+
{{#include ../exercises/chapter11/src/Main.purs:argParser}}
992996

993-
```haskell
994-
where
995-
env :: Y GameEnvironment
996-
env = gameEnvironment
997-
<$> yarg "p" ["player"]
998-
(Just "Player name")
999-
(Right "The player name is required")
1000-
false
1001-
<*> flag "d" ["debug"]
1002-
(Just "Use debug mode")
997+
{{#include ../exercises/chapter11/src/Main.purs:parserOptions}}
1003998
```
1004999

1005-
Here, the `gameEnvironment` function, which has the type `PlayerName -> Boolean -> GameEnvironment`, is lifted over `Y`. The two arguments specify how to read the player name and debug flag from the command line options. The first argument describes the player name option, which is specified by the `-p` or `--player` options, and the second describes the debug mode flag, which is turned on using the `-d` or `--debug` options.
1000+
Here `OP.info` combines a `Parser` with a set of options for how the help message is formatted. `env <**> OP.helper` takes any command line argument `Parser` named `env` and adds a `--help` option to it automatically. Options for the help message are of type `InfoMod`, which is a monoid, so we can use the `fold` function to add several options together.
1001+
1002+
The interesting part of our parser is constructing the `GameEnvironment`:
1003+
1004+
```haskell
1005+
{{#include ../exercises/chapter11/src/Main.purs:env}}
1006+
```
10061007

1007-
This demonstrates two basic functions defined in the `Node.Yargs.Applicative` module: `yarg`, which defines a command line option which takes an optional argument (of type `String`, `Number` or `Boolean`), and `flag` which defines a command line flag of type `Boolean`.
1008+
`player` and `debug` are both `Parser`s, so we can use our applicative operators `<$>` and `<*>` to lift our `gameEnvironment` function, which has the type `PlayerName -> Boolean -> GameEnvironment` over `Parser`. `OP.strOption` constructs a command line option that expects a string value, and is configured via a collection of `Mod`s folded together. `OP.flag` works similarly, but doesn't expect an associated value. `optparse` offers extensive [documentation](https://pursuit.purescript.org/packages/purescript-optparse) on different modifiers available to build various command line parsers.
10081009

10091010
Notice how we were able to use the notation afforded by the applicative operators to give a compact, declarative specification of our command line interface. In addition, it is simple to add new command line arguments, simply by adding a new function argument to `runGame`, and then using `<*>` to lift `runGame` over an additional argument in the definition of `env`.
10101011

10111012
## Exercises
10121013

1013-
1. (Medium) Add a new Boolean-valued property `cheatMode` to the `GameEnvironment` record. Add a new command line flag `-c` to the `yargs` configuration which enables cheat mode. The `cheat` command from the previous exercise should be disallowed if cheat mode is not enabled.
1014+
1. (Medium) Add a new Boolean-valued property `cheatMode` to the `GameEnvironment` record. Add a new command line flag `-c` to the `optparse` configuration which enables cheat mode. The `cheat` command from the previous exercise should be disallowed if cheat mode is not enabled.
10141015

10151016
## Conclusion
10161017

0 commit comments

Comments
 (0)