mirror of
https://github.com/urbit/shrub.git
synced 2024-11-24 04:58:08 +03:00
Add counter.txt
This commit is contained in:
parent
0d45aaac87
commit
85cb031ba0
431
pkg/arvo/neo/cod/std/src/fil/counter.txt
Normal file
431
pkg/arvo/neo/cod/std/src/fil/counter.txt
Normal file
@ -0,0 +1,431 @@
|
||||
# Chapter 1: Counter
|
||||
|
||||
One of the simplest shrubs imaginable is a counter that stores one number and takes one poke: make the number go up.
|
||||
|
||||
By the end of this chapter you'll understand the structure of a shrub, and how to write a trivial one of your own. This won't explain Shrubbery from first principles — you don't neeed to understand it from first principles to work with it — but you'll see how similar a shrub is to a Gall agent, and where they differ.
|
||||
|
||||
You'll also get a glimpse of how one shrub can accomodate various frontend interfaces. We'll make a simple HTMX frontend for Sky, a namespace browser and dev environment.
|
||||
|
||||
This chapter is the only real "tutorial" in that Counter doesn't currently exist on your ship. You can build Counter yourself following along this guide. The remaining three chapters will discuss shrubs that already exists in your `%neo` desk: Diary, Messenger, and Tasks.
|
||||
|
||||
In the Diary tutorial, you'll see how to write and read data to and from the namepsace. In Messenger, you'll see how shrubs can interact via the dependencies system. In the Tasks chapter, we'll look at how a full-featured UI works in the current system.
|
||||
|
||||
This chapter is focused on pattern-matching what you know about Gall to the new system.
|
||||
|
||||
## Counter in Gall and Shrubbery
|
||||
|
||||
Here's the Gall agent you'll reimplement in Shrubbery. It stores one number and takes one poke, `%inc`, to increment the number.
|
||||
|
||||
```
|
||||
/+ dbug, default-agent, verb
|
||||
|%
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
==
|
||||
+$ state-0
|
||||
$: %0
|
||||
value=@ud
|
||||
==
|
||||
+$ counter-action
|
||||
$% [%inc ~]
|
||||
==
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
%+ verb &
|
||||
%- agent:dbug
|
||||
=| state-0
|
||||
=* state -
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-fail on-fail:def
|
||||
++ on-save
|
||||
!>(state)
|
||||
::
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
:- ~
|
||||
%= this
|
||||
state !<(state-0 old)
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?+ mark
|
||||
(on-poke:def mark vase)
|
||||
::
|
||||
%noun
|
||||
=/ act
|
||||
!<(counter-action vase)
|
||||
?- -.act
|
||||
%inc
|
||||
:- ~
|
||||
%= this
|
||||
value +(value)
|
||||
==
|
||||
==
|
||||
==
|
||||
--
|
||||
```
|
||||
|
||||
Here's the same thing in Shrubery.
|
||||
|
||||
```
|
||||
/@ number
|
||||
/@ counter-diff
|
||||
^- kook:neo
|
||||
|%
|
||||
++ state
|
||||
^- curb:neo
|
||||
[%pro %number]
|
||||
++ poke
|
||||
^- (set stud:neo)
|
||||
(sy %counter-diff ~)
|
||||
++ deps
|
||||
^- deps:neo
|
||||
*deps:neo
|
||||
++ form
|
||||
^- form:neo
|
||||
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
|
||||
+* state !<(number state-vase)
|
||||
++ init
|
||||
|= old=(unit pail:neo)
|
||||
^- ((list card:neo) pail:neo)
|
||||
[~ (need old)]
|
||||
++ poke
|
||||
|= [=stud:neo vaz=vase]
|
||||
^- ((list card:neo) pail:neo)
|
||||
=/ act
|
||||
!<(counter-diff vaz)
|
||||
?> =(-.act %inc)
|
||||
[~ [%number !>(+(state))]]
|
||||
--
|
||||
--
|
||||
```
|
||||
|
||||
## Shrub structure
|
||||
|
||||
Let's look at the structure of `/imp/counter`.
|
||||
|
||||
```
|
||||
/@ number
|
||||
/@ counter-diff
|
||||
```
|
||||
|
||||
These lines import two types from our `/pro` folder: `number` and `counter-diff`. To import from `/pro` we use `/@` as a new Ford-style rune.
|
||||
|
||||
```
|
||||
,@ud
|
||||
```
|
||||
|
||||
```
|
||||
,[%inc ~]
|
||||
```
|
||||
|
||||
A shrub is a five-arm `|%` core — called a `kook:neo` — with an inner two-arm core called a `form:neo`. The `kook` defines type information about the shrub, and the inner `form` contains business logic.
|
||||
|
||||
At first glance the `kook` might look familiar to Gall developers, but this is all new logic defining 1) what's stored at this node in the namespace 2) what can be stored below this node, and 3) what we expect to be stored at existing nodes we declare as dependencies.
|
||||
|
||||
```
|
||||
:: $kook:neo
|
||||
|%
|
||||
::
|
||||
:: type this value in the namespace
|
||||
++ state
|
||||
!!
|
||||
::
|
||||
:: type acceptable requests to
|
||||
:: change this value in the namespace
|
||||
++ poke
|
||||
!!
|
||||
::
|
||||
:: constrain the state/pokes of the shrubs that
|
||||
:: can be created under this shrub in the namespace
|
||||
++ kids
|
||||
!!
|
||||
::
|
||||
:: declare the state/pokes we expect for existing shrubs
|
||||
:: whose state we will track, and whose state changes we
|
||||
:: will react to
|
||||
++ deps
|
||||
!!
|
||||
::
|
||||
:: handle state changes in this shrub,
|
||||
:: its kids, and its dependencies
|
||||
++ form
|
||||
!!
|
||||
--
|
||||
```
|
||||
|
||||
The `form` is where the Gall agent-like application logic lives. We only need two arms, which are slightly modified versions of `+on-init` and `+on-poke`.
|
||||
|
||||
```
|
||||
:: $form:neo
|
||||
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
|
||||
::
|
||||
:: like +on-init, run some logic when this shrub is created
|
||||
:: unlike +on-init, potentially accept some injected initial state
|
||||
++ init
|
||||
|= old=(unit pail:neo)
|
||||
^- ((list card:neo) pail:neo)
|
||||
!!
|
||||
::
|
||||
:: like +on-poke, run some logic when this shrub is poked
|
||||
++ poke
|
||||
|= [=stud:neo vaz=vase]
|
||||
^- ((list card:neo) pail:neo)
|
||||
!!
|
||||
--
|
||||
```
|
||||
|
||||
## Counter logic
|
||||
|
||||
Now that we understand the shape of a shrub, let's look at the application logic of the Counter shrub. You can copy the following into the relevant files or type it out for yourself.
|
||||
|
||||
There are lots of new types here which are flagged with the `:neo` suffix in code and documentation. We'll cover those in detail in the following chapters.
|
||||
|
||||
### /pro/number.hoon
|
||||
|
||||
```
|
||||
,@ud
|
||||
```
|
||||
|
||||
### /pro/counter-diff.hoon
|
||||
|
||||
```
|
||||
,[%inc ~]
|
||||
```
|
||||
|
||||
### /imp/counter.hoon
|
||||
|
||||
```
|
||||
/@ number :: import number type
|
||||
/@ counter-diff :: import counter-diff type
|
||||
::
|
||||
:: outer core
|
||||
^- kook:neo
|
||||
|%
|
||||
::
|
||||
:: the state of counter is a number
|
||||
++ state
|
||||
^- curb:neo
|
||||
[%pro %number]
|
||||
::
|
||||
:: the set of pokes counter takes only contains %counter-diff
|
||||
:: a stud:neo is like a mark
|
||||
++ poke
|
||||
^- (set stud:neo)
|
||||
(sy %counter-diff ~)
|
||||
::
|
||||
:: counter has no dependencies
|
||||
++ deps
|
||||
^- deps:neo
|
||||
*deps:neo
|
||||
::
|
||||
:: inner core
|
||||
++ form
|
||||
^- form:neo
|
||||
::
|
||||
:: the sample is populated with context like bowl, version number, and
|
||||
:: counter's current state
|
||||
|_ [=bowl:neo =aeon:neo =stud:neo state-vase=vase]
|
||||
::
|
||||
:: de-vase counter's state
|
||||
+* state !<(number state-vase)
|
||||
::
|
||||
:: +init, like +on-init
|
||||
++ init
|
||||
::
|
||||
:: return no cards and the initial given state
|
||||
:: pail:neo is a (pair stud:neo vase),
|
||||
:: like a cell of a mark and data
|
||||
|= old=(unit pail:neo)
|
||||
^- ((list card:neo) pail:neo)
|
||||
[~ (need old)]
|
||||
::
|
||||
:: +poke, like +on-poke
|
||||
++ poke
|
||||
|= [=stud:neo vaz=vase]
|
||||
^- ((list card:neo) pail:neo)
|
||||
::
|
||||
:: de-vase the poke
|
||||
=/ act
|
||||
!<(counter-diff vaz)
|
||||
::
|
||||
:: crash if we're not incrementing
|
||||
?> =(-.act %inc)
|
||||
::
|
||||
:: return no cards, return a (pair stud:neo vase)
|
||||
:: where the vase contains the incremented state
|
||||
[~ [%number !>(+(state))]]
|
||||
--
|
||||
--
|
||||
```
|
||||
|
||||
Once you've saved `/imp/counter.hoon` and the `/pro` files, run `|commit %base` and %neo will add it to its state. We can now interact with this shrub in the Dojo.
|
||||
|
||||
## Poking the shrub
|
||||
|
||||
A `card:neo` is a `(pair pith note)`.
|
||||
|
||||
A `pith` is a `(list iota)`, and an `iota` is either a `term` or a head-tagged noun. For instance:
|
||||
* `/examples/counter/one` would be represented as `~[%examples %counter %one]`.
|
||||
* `/~sampel/examples/counter/one` would be represented as `~[[%p ~sampel] %examples %counter %one]`.
|
||||
* `/~sampel/examples/counter/1` would be represented as `~[[%p ~sampel] %examples %counter [%ud 1]]`.
|
||||
|
||||
(You might also see a `pith` written in this irregular form `#/[p/our.bowl]/examples/counter/one`.)
|
||||
|
||||
Data in Shrubbery is stored by `pith`.
|
||||
|
||||
A `note` is one of the four types of command any shrub will accept.
|
||||
|
||||
```
|
||||
+$ note
|
||||
$% [%make made] :: create a shrub
|
||||
[%poke =pail] :: poke a shrub
|
||||
[%tomb cas=(unit case)] :: tombstone a case of the shrub
|
||||
[%cull ~] :: forward delete
|
||||
==
|
||||
```
|
||||
|
||||
Let’s `%make` a shrub at path `/foo/bar` from the Dojo, giving it an initial state of `0`. We’ll explain the structure of the `%make` note in more detail in the Diary tutorial.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %foo %bar] [%make %counter `[%number !>(0)] ~]]
|
||||
```
|
||||
|
||||
You should see `>> %make /foo/bar` in the Dojo if successful.
|
||||
|
||||
Now we can now send a `%poke` to the counter shrub at this path.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %foo %bar] [%poke [%counter-diff !>([%inc ~])]]]
|
||||
```
|
||||
|
||||
## Counter frontend in Sky
|
||||
|
||||
Shrubbery aims to be interface-agnostic. One part of that vision is `/con` files, which make it possible to convert data from one type to another. Here are Counter’s `/con` files.
|
||||
|
||||
### /con/number-htmx.hoon
|
||||
|
||||
This converts data stored as the `number` protocol (which is just a `@ud`) to the `htmx` protocol. When you open a shrub in Sky, Sky will attempt to convert its data to the `htmx` type (because Sky includes the [HTMX](https://htmx.org/) library in its frontend) using the appropriate `/con` file. In practice, this means that our `/con` file will take in our shrub's state (and bowl) and output some [Sail](https://docs.urbit.org/language/hoon/guides/sail) that interpolates the `number` in a basic interface consisting of a heading, the number itself, and one button to send an `%inc` poke to the Counter shrub.
|
||||
|
||||
```
|
||||
/@ number :: @ud
|
||||
:: import /lib/feather-icons (see feather-intro.txt)
|
||||
/- feather-icons
|
||||
:: declare that this is a conversion from number to HTMX
|
||||
:- [%number %$ %htmx]
|
||||
::
|
||||
:: this gate accepts a number and
|
||||
:: a gate that accepts a bowl:neo;
|
||||
:: we'll use bowl:neo to get the
|
||||
:: here.bowl of the shrub that's using this /con file
|
||||
|= =number
|
||||
|= =bowl:neo
|
||||
::
|
||||
:: this gate returns a manx, which is what Hoon uses
|
||||
:: to store dynamic XML nodes; in this case we'll use
|
||||
:: Sail to specify a manx that expects the HTMX library
|
||||
:: to be available on the frontend
|
||||
^- manx
|
||||
::
|
||||
:: open a <div class="p3 fc g2 ac br2">
|
||||
:: these utility classes are specified in feather.css,
|
||||
:: which this /con file expects on the frontend
|
||||
;div.p3.fc.g2.ac.br2
|
||||
:: <h1>Counter</h1>
|
||||
;h1: Counter
|
||||
:: <p>{number}</p>
|
||||
;p: {<number>}
|
||||
:: open a <form> with HTMX attributes
|
||||
;form
|
||||
::
|
||||
:: hx-post will issue a POST request to the provided
|
||||
:: url and swap the response into the DOM
|
||||
=hx-post "/neo/hawk{(en-tape:pith:neo here.bowl)}?stud=counter-diff"
|
||||
::
|
||||
:: hx-target specifies the target for hx-post's DOM
|
||||
:: swap: the element with class "loading"
|
||||
=hx-target "find .loading"
|
||||
::
|
||||
:: hx-swap specifies how the response to hx-post's
|
||||
:: request will be swapped in relative to the target
|
||||
=hx-swap "outerHTML"
|
||||
::
|
||||
:: here, the head attribute specifies the poke that
|
||||
:: hx-post will send to the target shrub; look at
|
||||
:: /con/node-counter-diff.hoon for more on =head
|
||||
=head "inc"
|
||||
::
|
||||
:: below, the classes "loaded", "loader", and
|
||||
:: "loading" provide loading spinner behavior on
|
||||
:: sending and receiving this form's POST request
|
||||
::
|
||||
:: <button class="bd1 br1 pr b1 hover loader">
|
||||
;button.bd1.br1.p2.b1.hover.loader
|
||||
:: <span class="loaded">Increment</span>
|
||||
;span.loaded: Increment
|
||||
:: <span class="loading">
|
||||
;span.loading
|
||||
:: import +loading sail from /lib/feather-icons
|
||||
;+ loading.feather-icons
|
||||
== :: </span>
|
||||
== :: </button>
|
||||
== :: </form>
|
||||
== :: </div>
|
||||
```
|
||||
|
||||
### /con/node-counter-diff.hoon
|
||||
|
||||
This is a more straightforward conversion from a dynamic XML node (in this case, HTMX), to a `%counter-diff`. Using a modified version of the [manx-utils](https://github.com/tinnus-napbus/manx-utils) Hoon library for brevity, we extract the XML node’s `head` attribute and use that to form the `%counter-diff`, which is `[%inc ~]`.
|
||||
|
||||
```
|
||||
/@ node :: manx
|
||||
/@ counter-diff :: [%inc ~]
|
||||
:: import /lib/manx-utils, which helps us work with XML
|
||||
/- manx-utils
|
||||
:: declare this is a conversion from node to counter-diff
|
||||
:- [%node %$ %counter-diff]
|
||||
|= =node
|
||||
^- counter-diff
|
||||
:: initiate the manx-utils door with node
|
||||
=/ mu ~(. manx-utils node)
|
||||
::
|
||||
:: got:mu gets an attribute from the manx by its name
|
||||
:: in this case, the =head specified in /con/number-htmx
|
||||
:: we expect the head from the manx to be %inc,
|
||||
:: but we could add more terms to that type union...
|
||||
=/ head (?(%inc) (got:mu %head))
|
||||
::
|
||||
:: return the [%inc ~] poke
|
||||
[head ~]
|
||||
```
|
||||
|
||||
## Testing the Counter in Sky
|
||||
|
||||
The Sky homepage shows you one tile for all of the shrubs who are the immediate children of your `/home` shurb, which was made for you upon booting `%neo` for the first time. You won’t see a Counter tile there because there is no `/counter` shrub beneath `/home`, so let’s make one.
|
||||
|
||||
```
|
||||
:neo &neo-card [~[[%p our] %home %counter] [%make %counter `[%number !>(0)] ~]]
|
||||
```
|
||||
|
||||
If you refresh your browser you should now see a tile labelled “counter”. Click there to see the Counter frontend from the `/con` file and increment the state of the `/counter` shrub.
|
||||
|
||||
## Building on the Counter
|
||||
|
||||
You should now be able to make some minor changes to the counter example above. Try the following:
|
||||
|
||||
- Initialize the shrub with a default state if the given `(unit vase)` in `+init` is empty.
|
||||
- Add pokes for `%dec`, `%add`, and `%sub`.
|
Loading…
Reference in New Issue
Block a user