|
0 |
# alivecoding: <mmm-embed wrap="raw" facet="description"></mmm-embed>
|
|
1 |
peristant expressions are an approach to livecoding that unifies direct
|
|
2 |
manipulation of a dataflow engine with a textual representation and
|
|
3 |
lisp-based programming language.
|
|
4 |
|
|
5 |
<mmm-embed wrap="raw" path="demo"></mmm-embed>
|
|
6 |
|
|
7 |
## shortcomings of repl-based programming
|
|
8 |
in repl-based environments, a scratch file is opened in a text editor. in it,
|
|
9 |
commands are staged and can be added, removed and edited without consequence.
|
|
10 |
the livecoding system generally has no knowledge about this scratch buffer at
|
|
11 |
all. the user is free to select and send individual commands (or groups of
|
|
12 |
commands) at any time and execute them by transmitting them to the server via
|
|
13 |
an editor plugin.
|
|
14 |
|
|
15 |
commands are incremental changes (deltas) that get sent to the server, which
|
|
16 |
keeps an entirely separate and invisible model of the project. generally no
|
|
17 |
feedback about the state of this model is made available to the user.
|
|
18 |
|
|
19 |
code is only executed when the user evaluates a block, although code run in
|
|
20 |
this fashion may cause other code to execute outside of the user-evaluated
|
|
21 |
execution flow via side effects, for example by registering a handler for
|
|
22 |
events such as incoming messages or scheduling execution based on system time.
|
|
23 |
these mechanisms however are implementation details within the code the user
|
|
24 |
executed originally, and no uniform mechanism for noticing, visualizing or
|
|
25 |
undoing these side-effects exists.
|
|
26 |
|
|
27 |
this design has the following consequences:
|
|
28 |
|
|
29 |
- the view of the scratch buffer is not correlated with the code and state the
|
|
30 |
server is currently executing. this results in overhead for keeping the
|
|
31 |
mental synchronized with what the system is actually performing for the user,
|
|
32 |
but also makes it much harder for the audience to follow along.
|
|
33 |
- sessions cannot be reopened reliably, because the state of the server depends
|
|
34 |
on the full sequence of commands that were sent to the server in order, which
|
|
35 |
is not represented in the scratch buffer.
|
|
36 |
- if parts of the execution model on the server have not been explicitly
|
|
37 |
labelled (i.e. assigned to a variable) in the textual representation, often
|
|
38 |
many potentially important actions for modifying the current behaviour are
|
|
39 |
unavailable: for example long-running sounds may not be cancellable, effects'
|
|
40 |
parameters may not be adjustable without recreating the signal chain, etc.
|
|
41 |
|
|
42 |
## persistent expressions
|
|
43 |
the *persistent expression* paradigm, on the other hand, reconciles the user-
|
|
44 |
facing, text-based representation of the system and the server-internal model
|
|
45 |
and execution flow.
|
|
46 |
|
|
47 |
### execution flow
|
|
48 |
code execution happens in two different phases alternatingly: at *eval-time*,
|
|
49 |
whenever the buffer is (re)evaluated; and at *run-time*, continuously between
|
|
50 |
evaluations.
|
|
51 |
|
|
52 |
at *eval-time*, execution is analogous to common functional and lisp-style
|
|
53 |
languages. expressions are evaluated depth-first starting from the root.
|
|
54 |
for each expression, the head of the expression is first evaluated, and
|
|
55 |
depending on the type of that subexpression different actions are taken. in the
|
|
56 |
general case, the head of an expression is an *op* (operator) type, an instance
|
|
57 |
of which will continue to run at *run-time*. in this case, all other arguments
|
|
58 |
are then evaluated and passed to the *op* instance, which is either created or
|
|
59 |
reused (see below).
|
|
60 |
on the other hand, some expressions (for example `def`, `use`, ...) do not
|
|
61 |
execute at *run-time*, but cause *eval-time* side-effects like declaring a
|
|
62 |
symbol in the active scope. because *eval-time* execution only happens once and
|
|
63 |
in a deterministic order, and no *eval-time* state persists across evaluations,
|
|
64 |
despite these side-effects, the *eval-time* execution is equivalent to
|
|
65 |
functionally pure execution with an implicit scope parameter.
|
|
66 |
|
|
67 |
unlike normal lisps, when evaluating expressions, not only a value is
|
|
68 |
generated. in parallel to the tree of return values, a tree of *run-time*
|
|
69 |
dependencies is built, that tracks all instantiated *op*s and their inputs.
|
|
70 |
|
|
71 |
at *run-time*, *op* instances update based on this dependency tree. starting
|
|
72 |
from a periodic root event polled by the interpreter, dependent *op*s are
|
|
73 |
executed (following the outside-in, depth-first order that the dependencies have
|
|
74 |
been created in at *eval-time*). *op*s whose inputs are unchanged and 'pure'
|
|
75 |
subtrees that do not have any dependency on the root event are not executed.
|
|
76 |
in this way, the *run-time* behaviour of the system is that of a event-driven
|
|
77 |
dataflow language with clearly defined execution flow.
|
|
78 |
|
|
79 |
### expression tagging
|
|
80 |
in order to maintain the congruency between the representations across edits
|
|
81 |
and reevaluations, the identity of individual expressions is tracked using
|
|
82 |
tags. tags are noted using unique numbers in square brackets before the head of
|
|
83 |
expressions (e.g. `([1]head arg1 arg2...)`) and are optional when parsed.
|
|
84 |
|
|
85 |
at *eval-time* (see below), every expression that is not tagged will be
|
|
86 |
assigned a new unique tag number. 'cloned' expressions, such as the expressions
|
|
87 |
from a function definition body, are assigned composite tags that can be noted
|
|
88 |
as a list of tags joined by periods (e.g. `[2.1]`):
|
|
89 |
|
|
90 |
```
|
|
91 |
([1]defn add-two-and-multiply (a b)
|
|
92 |
([2]mul b ([3]add a 2)))
|
|
93 |
|
|
94 |
([4]add-two-and-multiply 1 2)
|
|
95 |
([5]add-two-and-multiply 3 4)
|
|
96 |
```
|
|
97 |
|
|
98 |
will be expanded (at *eval-time*) to approximately<span class="sidenote">
|
|
99 |
the actual implementation does not actually create sub expressions as shown
|
|
100 |
here, but the results behave equivalently.</span>:
|
|
101 |
|
|
102 |
```
|
|
103 |
(do
|
|
104 |
(def a 1
|
|
105 |
b 2)
|
|
106 |
([4.2]mul a ([4.3]add b 2)))
|
|
107 |
(do
|
|
108 |
(def a 3
|
|
109 |
b 4)
|
|
110 |
([5.2]mul a ([5.3]add b 2)))
|
|
111 |
```
|
|
112 |
|
|
113 |
the expression tags are used to associate the *run-time* representations (*op*
|
|
114 |
instances) of expressions with their textual representations, and track their
|
|
115 |
identity as the user changes the code. when the code is evaluated, *op*s are
|
|
116 |
instantiated whenever the expression was previously untagged, or when the head
|
|
117 |
of the expression no longer resolves to the same value. otherwise, the previous
|
|
118 |
*op* instance continues to exist and parameter changes are forward to it. *op*s
|
|
119 |
that are no longer referenced in the code are destroyed.
|
|
120 |
|
|
121 |
### benefits
|
|
122 |
this approach combines the benefits of dataflow programming for livecoding with
|
|
123 |
those of a textual representation and the user-controlled evaluation moment.
|
|
124 |
|
|
125 |
dataflow:
|
|
126 |
|
|
127 |
- direct manipulation of individual parameters of a system without disturbing
|
|
128 |
the system at large
|
|
129 |
- execution and dataflow are aligned and evident in the editable representation
|
|
130 |
- state is isolated and compartmentalized in locally
|
|
131 |
- opportunity to visualize dataflow and local state<span class="sidenote">
|
|
132 |
visualizing state of individual *op*s in editor-dependent and editor-agnostic
|
|
133 |
ways that integrate with the textual representation is an ongoing research
|
|
134 |
direction of this project</span>
|
|
135 |
|
|
136 |
textual representation and user-controlled evaluation moment:
|
|
137 |
|
|
138 |
- high information density
|
|
139 |
- fast editing experience
|
|
140 |
- accessibility and editability from a wide range of tools (any text editor)
|
|
141 |
- ability to harness powerful meta-programming facilities (from Lisp)
|
|
142 |
- complex changes can be made without intermittently disrupting the system
|