Skip to content

Commit 7ab305d

Browse files
committed
Rework Fundamentals section
Add 'REPL' and 'Modules' sections.
1 parent 99d279f commit 7ab305d

File tree

1 file changed

+124
-91
lines changed

1 file changed

+124
-91
lines changed

index.adoc

Lines changed: 124 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ implement a minimal '`Hello World`' application. While it may be
115115
tempting to skip ahead, a sound understanding of how to use Duct will
116116
make later sections easier to follow.
117117

118-
=== Hello World
118+
=== First Steps
119119

120120
As mentioned previously, Duct uses a file, `duct.edn`, to define the
121121
structure of your application. We'll begin by adding a new component
@@ -169,140 +169,173 @@ Hello World
169169

170170
Congratulations on your first Duct application!
171171

172-
=== Component Options
172+
=== The REPL
173173

174-
One advantage of having a configuration file is that we can customize
175-
the behavior of components. Let's change our `hello` function to take
176-
a `:name` option.
174+
Duct has two ways of running your application: `--main` and `--repl`.
177175

178-
[,clojure]
179-
----
180-
(ns tutorial.print)
176+
In the previous section we started the application with `--main`, which
177+
will *initiate* the system defined in the configuration file, and *halt*
178+
the system when the process terminates.
179+
180+
The REPL is an interactive development environment.
181181

182-
(defn hello [{:keys [name]}]
183-
(println "Hello" name))
182+
[,shell]
183+
----
184+
$ duct --repl
185+
✓ Loading REPL environment...
186+
[Repl balance] Type :repl/help for online help info
187+
user=>
184188
----
185189

186-
Now that `hello` expects an option, we'll need to add it to the
187-
`duct.edn` file.
190+
In the REPL environment the system will not be initiated automatically.
191+
Instead, we use the inbuilt `(go)` function.
188192

189193
[,clojure]
190194
----
191-
{:system
192-
{:tutorial.print/hello {:name "World"}}}
195+
user=> (go)
196+
Hello World
197+
:initiated
193198
----
194199

195-
Naturally this produces the same result as before when we run the
196-
application.
200+
The REPL can be left running while source files updated. The `(reset)`
201+
function will halt the running system, reload any modified source files,
202+
then initiate the system again.
197203

198-
[,shell]
204+
[,clojure]
199205
----
200-
$ duct --main
201-
✓ Initiating system...
206+
user=> (reset)
207+
:reloading (tutorial.print)
202208
Hello World
209+
:resumed
203210
----
204211

205-
=== Variables
206-
207-
Sometimes we want to supply options from an external source, such as an
208-
environment variable or command line option. Duct allows variables, or
209-
*vars*, to be defined in the `duct.edn` configuration.
210-
211-
Let's add a var to our configuration file.
212+
The configuration defined by `duct.edn` can be accessed with `config`,
213+
and the running system can be accessed with `system`.
212214

213215
[,clojure]
214216
----
215-
{:vars
216-
{name {:arg name, :env NAME, :type :str, :default "World"
217-
:doc "The name of the person to greet"}}
218-
:system
219-
{:tutorial.print/hello {:name #ig/var name}}}
217+
user=> config
218+
#:tutorial.print{:hello {}}
219+
user=> system
220+
#:tutorial.print{:hello nil}
220221
----
221222

222-
This defines a var called `name` with two sources. In order of priority:
223+
=== Modules
223224

224-
. A command-line argument `--name` (set via `:arg`)
225-
. An environment variable `$NAME` (set via `:env`)
225+
A *module* groups multiple components together. Duct provides a number
226+
of pre-written modules that implement common functionality. One of these
227+
modules is `:duct.module/logging`.
226228

227-
This value can be inserted into the system map with the `#ig/var` data
228-
reader. If the variable has no value, an error will be raised, so it's a
229-
good idea to set a default value using the `:default` key.
229+
We'll first add the dependency to `deps.edn`.
230230

231-
NOTE: The '`ig`' in `#ig/var` stands for _Integrant_. This is the
232-
library that Duct relies on to turn configurations into running
233-
applications.
231+
[,clojure]
232+
----
233+
{:deps {org.duct-framework/main {:mvn/version "0.1.0"}
234+
org.duct-framework/module.logging {:mvn/version "0.6.4"}}
235+
:aliases {:duct {:main-opts ["-m" "duct.main"]}}}
236+
----
237+
238+
Then we'll add the module to the `duct.edn` configuration.
234239

235-
The `:type` of a var determines the data type it should be coerced into.
236-
Duct supports three types natively: `:str`, `:int` and `:float`. The
237-
default type when the key is omitted is `:str`.
240+
[,clojure]
241+
----
242+
{:system
243+
{:duct.module/logging {}
244+
:tutorial.print/hello {}}}
245+
----
238246

239-
Duct integrates these vars into its help message. The `:doc` option
240-
specifies a description of the var.
247+
Before the components are initiated, modules are *expanded*. We can see
248+
what this expansion looks like by using the `--show` flag. This will
249+
print out the expanded configuration instead of initiating it.
241250

242-
[,shell,highlight=13]
251+
[,shell]
243252
----
244-
$ duct --help
245-
Usage:
246-
clojure -M:duct [--main | --repl]
247-
Options:
248-
-c, --cider Start an NREPL server with CIDER middleware
249-
--init Create a blank duct.edn config file
250-
-p, --profiles PROFILES A concatenated list of profile keys
251-
-n, --nrepl Start an NREPL server
252-
-m, --main Start the application
253-
-r, --repl Start a command-line REPL
254-
-s, --show Print out the expanded configuration and exit
255-
-v, --verbose Enable verbose logging
256-
-h, --help Print this help message and exit
257-
--name NAME The name of the person to greet
253+
$ duct --main --show
254+
{:duct.logger/simple {:appenders [{:type :stdout}]}
255+
:tutorial.print/hello {}}
258256
----
259257

260-
The var can then be specified at the command line to produce different
261-
results.
258+
The logging module has been replaced with the `:duct.logger/simple`
259+
component.
260+
261+
The `--show` flag also works with the `--repl` command.
262262

263263
[,shell]
264264
----
265-
$ duct --main --name=Clojurian
266-
✓ Initiating system...
267-
Hello Clojurian
265+
$ duct --repl --show
266+
{:duct.logger/simple
267+
{:appenders
268+
[{:type :stdout, :brief? true, :levels #{:report}}
269+
{:type :file, :path "logs/repl.log"}]}
270+
:tutorial.print/hello {}}
271+
----
268272

269-
$ NAME=Clojurist duct --main
270-
✓ Initiating system...
271-
Hello Clojurist
273+
But wait a moment, why is the expansion of the configuration different
274+
depending on how we run Duct? This is because the `--main` flag has an
275+
implicit `:main` profile, and the `--repl` flag has an implicit `:repl`
276+
profile.
277+
278+
The `:duct.module/logging` module has different behaviors depending on
279+
which profile is active. When run with the `:main` profile, the logs
280+
print to STDOUT, but this would be inconveniently noisy when using a
281+
REPL. So when the `:repl` profile is active, most of the logs are sent
282+
to a file, `logs/repl.log`.
283+
284+
In order to use this module, we need to connect the logger to our
285+
'`hello`' component. This is done via a *ref*.
286+
287+
[,clojure]
288+
----
289+
{:system
290+
{:duct.module/logging {}
291+
:tutorial.print/hello {:logger #ig/ref :duct/logger}}}
272292
----
273293

274-
=== References
294+
The `#ig/ref` data reader is used to give the '`hello`' component access
295+
to the logger. We use `:duct/logger` instead of `:duct.logger/simple`,
296+
as keys have a logical hierarchy, and `:duct/logger` fulfils a role
297+
similar to that of an interface or superclass.
275298

276-
A Duct system can have multiple components, and components can
277-
communicate via references, which are divided into *refs* and
278-
*refsets*. A ref references exactly one other component; a refset
279-
references zero or more components.
299+
NOTE: The '`ig`' in `#ig/var` stands for _Integrant_. This is the
300+
library that Duct relies on to turn configurations into running
301+
applications.
280302

281-
To demonstrate how refs work, we'll divide our '`Hello World`'
282-
application into two functions.
303+
Now that we've connected the components together in the configuration
304+
file, it's time to replace the `println` function with the Duct logger.
283305

284306
[,clojure]
285307
----
286-
(ns tutorial.print)
308+
(ns tutorial.print
309+
(:require [duct.logger :as log]))
310+
311+
(defn hello [{:keys [logger]}]
312+
(log/report logger ::hello {:name "World"}))
313+
----
314+
315+
The `duct.logger/report` function is used to emit a log at the `:report`
316+
level. This is a high-priority level that should be used sparingly, as
317+
it also prints to STDOUT when using the REPL.
287318

288-
(defn printer [{:keys [prefix]}]
289-
(partial println prefix))
319+
You may have noticed that we've replaced the `"Hello World"` string with
320+
a keyword and a map: `::name {:name "World"}`. This is because Duct is
321+
opinionated about logs being data, rather than human-readable strings. A
322+
Duct log message consists of an *event*, a qualified keyword, and a map
323+
of *event data*, which provides additional information.
290324

291-
(defn hello [{:keys [name output]}]
292-
(output "Hello" name))
325+
When we run the application, we can see what this produces.
326+
327+
[,shell]
328+
----
329+
$ duct --main
330+
✓ Initiating system...
331+
2024-11-23T18:59:14.080Z :report :tutorial.print/hello {:name "World"}
293332
----
294333

295-
The `printer` component returns a function that prints its arguments
296-
with a custom prefix. The `hello` component takes a `:output` option
297-
that it uses to output a message. In order to connect these two
298-
components in the `duct.edn` file, we use the `#ig/ref` data reader.
334+
When using the REPL, we get a more concise message.
299335

300-
[,clojure]
336+
[,shell]
301337
----
302-
{:system
303-
{:tutorial.print/printer
304-
{:prefix ">>"}
305-
:tutorial.print/hello
306-
{:name "World"
307-
:output #ig/ref :tutorial.print/printer}}}
338+
user=> (go)
339+
:initiated
340+
:tutorial.print/hello {:name "World"}
308341
----

0 commit comments

Comments
 (0)