This guide provides examples, tutorials, notes and other documentation that do not belong in The Neuron Reference.
Neuron is a series of Racket libraries that provide a consistent API over a spectrum of functionality related to the creation, operation, integration, and evolution of concurrent, distributed, and decentralized run time environments and applications. At its core is a communication-based concurrency model and a structural pattern-based DSL for working with composable evaluators.
Neuron uses a concurrency model of lightweight processes communicating over named, synchronous channel-like mediators. Neuron processes extend Racket threads with support for life cycle hooks and two orthogonal lines of communication. In other words, a process is like a thread that can clean up after itself and keep “secrets.”
A process is created in the starting state when another process attempts to spawn a new thread of execution. The requesting process blocks until the new process is alive and a fresh process descriptor for it has been returned.
A process stays alive until its thread of execution terminates. A process can terminate itself, either by reaching the end of its program or by issuing a quit or die command. A process can also use the stop or kill command to terminate any process it holds a process descriptor for.
When a process reaches the end of its program or is terminated by quit or stop, it enters the stopping state while it calls its on-stop hook. When a process reaches the end of its on-stop hook or is terminated by a die or kill command, it enters the dying state while it calls its on-dead hook. A process is dead when its on-dead hook returns.
> (wait (start (start (process (λ () (displayln 'ALIVE))) #:on-stop (λ () (displayln 'STOP-1)) #:on-dead (λ () (displayln 'DEAD-1))) #:on-stop (λ () (displayln 'STOP-2)) #:on-dead (λ () (displayln 'DEAD-2))))
The on-dead hook is for freeing resources no longer needed by any process. Neuron uses the on-dead hook internally to terminate network listeners and kill sub-processes. This hook runs unconditionally and can’t be canceled.
The on-stop hook is for extra or optional clean-up tasks. Neuron uses the on-stop hook to close ports, terminate network connections, and stop sub-processes. For example, some codecs close input ports and output ports when stopped but not when killed so they can be swapped out mid-stream or restarted after errors have been handled.
The deadlock function waits for the current process to terminate, allowing the computation to diverge efficiently. It can be used as a termination “latch” to prevent the current process from ending until stopped or killed.
> (kill (start (start (process deadlock) #:on-stop (λ () (displayln 'STOP-1)) #:on-dead (λ () (displayln 'DEAD-1))) #:on-stop (λ () (displayln 'STOP-2)) #:on-dead (λ () (displayln 'DEAD-2))))
Applying a process descriptor to an argument list invokes its command handler, a simple dispatch mechanism. Because the command handler is installed while a process is starting, it can have direct access to the internal state of the process via the constructing closure.
Neuron uses the command handler to provide simple properties and methods.
> (define π (let ([env #hash(((a b) . 1) ((c) . 2))]) (start (process deadlock) #:command (λ args (hash-ref env args #f))))) > (π 'a 'b)
> (π 'c)
> (π 'd)
Processes can be combined to provide restricted or revocable access to others.
> (define π (sexp-codec (string-socket #:in "12 34 56" #:out #t))) > (define to-π (proxy-to π)) > (define from-π (proxy-from π)) > (recv from-π)
> (give to-π 'abc)
> (get-output-string (π 'socket))
> (or (sync/timeout 0 (recv-evt to-π)) (sync/timeout 0 (give-evt from-π)))
> (define A (process (λ () (define π-ref (take)) (displayln `(IN-A ,(recv π-ref))) (emit) (take) ; B kills π-ref (displayln `(IN-A ,(recv π-ref))))))
> (define B (process (λ () (define π (sexp-codec (string-socket #:in "12 34 56"))) (define π-ref (proxy π)) (give A π-ref) (recv A) ; A reads live π-ref (kill π-ref) (give A) (wait A) ; A reads dead π-ref (displayln `(IN-B ,(recv π)))))) > (sync (async-void A B))
Processes and threads can be combined.
and the number
are terms because they are literal values. The structures
#hasheq((a-symbol . 123))
A stepper is a function that maps one term to another. For example,
(case-lambda [(a) 1] [(b) 2] [else 0])
maps any term to a number between 0 and 2. Similarly,
(match-lambda [1 'a] [2 'b] [_ 'z])
maps any term to 'a, 'b, or 'z. A more realistic example is values, which maps every term to itself; or the function
(define step (match-lambda [(list (? term? e1) (? term? e2)) #:when (not (value? e1)) (list (step e1) e2)] [(list (? value? v1) (? term? e2?)) #:when (not (value? e2)) (list v1 (step e2))] [(list `(λ ,(? symbol? x11) ,(? term? e12)) (? value? v2)) (substitute e12 x11 v2)] [_ 'stuck]))
a small-stepper for the untyped lambda calculus.