| name | hoon-development |
| description | Write and compile Hoon code following Nock conventions and NockApp patterns. Use when working with Hoon kernel code, modifying state structures, adding pokes/peeks, or debugging Hoon compilation. |
Hoon Development Guide
This skill helps you write idiomatic Hoon code for the Prover NockApp kernel.
Hoon Basics
File Structure
:: Comments start with ::
:: Documentation comments
::
|% :: Core definition (library of types and functions)
+$ type-name :: Type definition
structure
--
|_ =state :: Door (object with state)
++ arm-name :: Arm (method/function)
code
--
Type Definitions
:: Atoms (basic types)
@ud :: Unsigned decimal (0, 1, 42)
@t :: Text/cord ('hello')
@tas :: Term/symbol (%groth16, %pending)
@da :: Timestamp/date (~2024.1.1)
:: Complex types
(list @t) :: List of text
(map @ud @t) :: Map from numbers to text
(unit @t) :: Optional text (~ or [~ 'value'])
?(%a %b %c) :: Union of terms
:: Structures
+$ person
$: name=@t
age=@ud
email=@t
==
:: Unions (discriminated)
+$ action
$% [%create name=@t]
[%update id=@ud name=@t]
[%delete id=@ud]
==
State Management
State Structure
Your NockApp state should be versioned:
+$ state
$: %v1 :: Version marker
data=(map @ud entry) :: Your data
next-id=@ud :: ID counter
config=settings :: Configuration
==
Accessing State Fields
:: Read field
=/ current-id next-id.state
:: Read nested field
=/ first-entry (~(get by data.state) 1)
Updating State Immutably
IMPORTANT: Never mutate state directly. Always create a new state.
:: Update single field
state(next-id +(next-id.state))
:: Update multiple fields
state(data new-data, next-id +(next-id.state))
:: Update with complex computation
=/ new-data (~(put by data.state) key value)
=/ new-id +(next-id.state)
state(data new-data, next-id new-id)
The ++poke Pattern
All input commands flow through ++poke:
++ poke
|= =cause :: Take a cause as input
^- [effects=(list effect) _state] :: Return effects and new state
?- -.cause :: Pattern match on cause tag
%command-one
:: Handle command one
:_ state :: Return state first (reversed)
:~ [%http-response 200 body] :: Effect list
==
%command-two
:: Handle command two
=/ updated-state state(...)
:_ updated-state
:~ [%http-response 201 body]
[%log 'Action completed']
==
==
Key Patterns
Extract inputs from cause:
=/ input-field field.causeValidate inputs:
?: =(input-field 0) :_ state [%http-response 400 '{"error":"Field cannot be zero"}']~Compute new values:
=/ new-id +(next-id.state) =/ entry [id data timestamp]Update state:
=/ updated-state state(data (~(put by data.state) key entry))Return effects and state:
:_ updated-state :~ [%http-response 200 body] [%log 'Success'] ==
Map Operations
Common Map Functions
:: Insert or update
=/ new-map (~(put by old-map) key value)
:: Lookup (returns unit)
=/ maybe-value (~(get by map) key)
?~ maybe-value
:: Handle not found
:: Handle found, use u.maybe-value
:: Delete
=/ new-map (~(del by old-map) key)
:: Check existence
=/ exists (~(has by map) key)
:: Convert to list
=/ pairs ~(tap by map) :: List of [key value]
:: Get size
=/ count ~(wyt by map)
Safe Map Access Pattern
=/ maybe-entry (~(get by snarks.state) id.cause)
?~ maybe-entry
:: Not found - return 404
:_ state
:~ [%http-response 404 '{"error":"Not found"}']
==
:: Found - use u.maybe-entry
=/ entry u.maybe-entry
:: ... continue with entry
List Operations
:: Create list
=/ my-list ~['a' 'b' 'c']
=/ empty-list ~
:: Add to front
=/ new-list [item my-list]
:: Iterate/map
=/ doubled (turn my-list |=(x=@ud (mul x 2)))
:: Filter
=/ evens (skim my-list |=(x=@ud =(0 (mod x 2))))
:: Fold/reduce
=/ sum (roll my-list add)
:: Check if empty
?~ my-list
:: Empty case
:: Non-empty case, i.my-list is head, t.my-list is tail
Control Flow
Conditionals
:: If-then-else (for values)
?: condition
value-if-true
value-if-false
:: If-null check
?~ maybe-value
handle-null
handle-value :: u.maybe-value is the unwrapped value
:: Pattern matching
?- value
%option-a result-a
%option-b result-b
%option-c result-c
==
Variable Binding
:: Bind variable
=/ name value
:: Bind with type
=/ name ^- @ud 42
:: Sequential bindings
=/ a 1
=/ b 2
=/ c (add a b)
c :: Returns 3
Text and String Operations
Creating Text
:: Literal cord (compile-time)
'hello'
:: Convert with crip (runtime)
(crip "hello")
:: Convert tape to cord
=/ t "hello"
=/ c (crip t)
Text Formatting
:: Format number to text
=/ num-text (scow %ud 42) :: "42"
=/ id-text (scow %ud id.cause) :: Format ID
:: Convert text to tape for manipulation
=/ tape-form (trip 'hello') :: "hello"
:: Concatenate (as tapes)
=/ result (weld "hello" " world")
=/ cord-result (crip result) :: 'hello world'
JSON-ish String Building (Simplified)
:: Simple response
'{"success":true,"id":42}'
:: With interpolation (manual)
=/ id-str (scow %ud id)
(crip (weld "{\"id\":" (weld id-str "}")))
Note: The prover uses helper functions for JSON formatting. See ++format-submit-response in prover.hoon.
Common Helper Functions
From the Prover Codebase
++ format-submit-response
|= id=@ud
^- tape
(weld "{\"success\":true,\"id\":" (weld (trip (scow %ud id)) "}"))
++ format-snark-detail
|= [id=@ud entry=snark-entry]
^- tape
:: Build JSON response string
...
Creating Your Own Helpers
++ build-error-response
|= message=@t
^- @t
(crip (weld "{\"error\":\"" (weld (trip message) "\"}")))
++ format-list-response
|= items=(list @t)
^- @t
:: Convert list to JSON array
...
Type Hints and Casts
:: Specify return type
=/ value ^- @ud 42
:: Cast to type
=/ entry ^- snark-entry
[id proof inputs vk system submitter now %pending ~ notes]
:: Useful for complex structures
=/ new-state ^- state
state(data new-data, next-id new-id)
The Bowl (Context)
In NockApp pokes, you have access to a bowl with context:
++ poke
|= [=cause =bowl:cask] :: Bowl is available
^- [(list effect:cask) _state]
:: Access bowl fields:
now.bowl :: Current timestamp (@da)
our.bowl :: Our identity
:: etc.
Common use: Get current timestamp
=/ timestamp now.bowl
=/ entry [id data timestamp ...]
Note: In the prover, we use now directly (it's implicitly from bowl).
Error Handling
Validation Pattern
:: Check condition
?: (lth id.cause 1)
:: Return error
:_ state
:~ [%http-response 400 '{"error":"ID must be positive"}']
==
:: Continue with valid input
...
Not-Found Pattern
=/ maybe-entry (~(get by data.state) id.cause)
?~ maybe-entry
:_ state
:~ [%http-response 404 '{"error":"Not found"}']
[%log (crip "Entry {(scow %ud id.cause)} not found")]
==
:: Entry exists, use u.maybe-entry
...
Effects
Common effect types:
[%http-response code=@ud body=@t] :: HTTP response
[%log message=@t] :: Log message
[%error message=@t] :: Error log
Multiple Effects
:_ state
:~ [%http-response 201 body]
[%log 'SNARK submitted']
[%log (crip "ID: {(scow %ud new-id)}")]
==
Compiling and Testing
Compilation
# Compile Hoon to Nock
hoonc prover/hoon/prover.hoon -o prover/out.jam
# Check for syntax errors
hoonc --check prover/hoon/prover.hoon
Common Compilation Errors
Mint failure: Type mismatch
- Check your type annotations (
^- type value) - Ensure structure matches type definition
- Check your type annotations (
Find failure: Unknown name
- Check spelling of variables and arms
- Ensure variables are in scope (
=/binding)
Nest failure: Type doesn't fit expected shape
- Check structure field order
- Verify all required fields are present
Debugging Tips
Add log effects:
[%log 'Reached this point'] [%log (crip "Value: {(scow %ud value)}")]Simplify complex expressions:
:: Instead of nested calls =/ step1 (operation1 input) =/ step2 (operation2 step1) =/ step3 (operation3 step2) step3Check types with hints:
=/ value ^- expected-type expression
Best Practices
- Always version your state:
$: %v1 ... - Use type hints for complex values:
^- type value - Validate inputs early: Check before updating state
- Keep arms focused: One responsibility per arm
- Use helper functions: Extract formatting and validation
- Log important operations: Help with debugging
- Handle errors explicitly: Don't assume success
- Update state immutably: Use
state(field new-value) - Test incrementally: Compile after each change
- Follow existing patterns: Match the style in prover.hoon
Quick Reference
Variable Binding
=/ name value :: Bind variable
=/ name ^- @ud 42 :: With type hint
State Update
state(field value) :: Single field
state(field1 val1, field2 val2) :: Multiple fields
Map Operations
(~(put by map) key value) :: Insert/update
(~(get by map) key) :: Lookup (returns unit)
(~(del by map) key) :: Delete
~(tap by map) :: Convert to list
Conditionals
?: condition true-branch false-branch :: If-then-else
?~ maybe-val null-branch value-branch :: If-null
?- value %a a %b b == :: Pattern match
Effects
:_ state
:~ [%http-response 200 '{"ok":true}']
[%log 'Done']
==
Example: Adding a New Poke
Let's add a %count-snarks command:
:: 1. Add to cause type
+$ cause
$% ...
[%count-snarks ~]
==
:: 2. Implement in ++poke
++ poke
|= [=cause =bowl:cask]
^- [(list effect:cask) _state]
?- -.cause
...
%count-snarks
=/ count ~(wyt by snarks.state)
=/ response (crip (weld "{\"count\":" (weld (trip (scow %ud count)) "}")))
:_ state
:~ [%http-response 200 response]
[%log (crip "Total SNARKs: {(scow %ud count)}")]
==
==
Done! Compile and test:
hoonc prover/hoon/prover.hoon -o prover/out.jam
Additional Resources
- Hoon School: https://developers.urbit.org/guides/core/hoon-school
- Hoon Standard Library: https://developers.urbit.org/reference/hoon
- NockApp Documentation: Check the nockup project docs
Common Patterns from Prover Codebase
ID Generation
=/ new-id next-id.state
:: ... use new-id ...
state(next-id +(next-id.state))
Building Entries
=/ entry ^- snark-entry
:* id
proof
inputs
vk
system
submitter
now
%pending
~
notes
==
Map Insert Pattern
=/ updated-state
state(snarks (~(put by snarks.state) new-id entry), next-id +(next-id.state))
Response with State
:_ updated-state
:~ [%http-response 201 (crip (format-submit-response new-id))]
[%log (crip "SNARK #{(scow %ud new-id)} submitted")]
==