-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathalter.html
349 lines (334 loc) · 14 KB
/
alter.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
<html>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<head>
<title>
leontrolski - containing mutable data
</title>
<style>
body {margin: 5% auto; background: #fff7f7; color: #444444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.8; max-width: 63%;}
@media screen and (max-width: 800px) {body {font-size: 14px; line-height: 1.4; max-width: 90%;}}
pre {width: 100%; border-top: 3px solid gray; border-bottom: 3px solid gray;}
a {border-bottom: 1px solid #444444; color: #444444; text-decoration: none; text-shadow: 0 1px 0 #ffffff; }
a:hover {border-bottom: 0;}
.inline {background: #b3b2b226; padding-left: 0.3em; padding-right: 0.3em; white-space: nowrap;}
blockquote {font-style: italic;color:black;background-color:#f2f2f2;padding:2em;}
details {border-bottom:solid 5px gray;}
</style>
<link href="https://unpkg.com/[email protected]/themes/prism-vs.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/components/prism-core.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/plugins/autoloader/prism-autoloader.min.js">
</script>
</head>
<body>
<a href="index.html">
<img src="pic.png" style="height:2em">
⇦
</a>
<p><i>2021-02-21</i></p>
<h1>
Can we contain mutable data in imperative languages?
</h1>
<p>
Using immutable data confers all sorts of advantages, in practice, the main ones are:
</p>
<ul>
<li>
It removes nasty "action at a distance" bugs.
</li>
<li>
It opens the door for easy comparison of values.
</li>
<li>
It can make concurrent programming easier.
</li>
</ul>
<p>
This post briefly surveys the territory, then proposes a small syntactic addition to Javascript that would help constrain mutability, while having a clear transition path from existing code (no funky FP stuff). The examples are in Javascript, but the same concept could be ported to Python and other high level languages with mutable data.
</p>
<em>
If you're already familiar with immutable libraries in Javascript, feel free to jump to the <a href="#proposal">
proposal
</a>
.
</em>
<h2>
What do we have now?
</h2>
<p>
The most well known <a href="https://github.com/immutable-js/immutable-js">
immutable library
</a>
gives us a smorgasbord of new data structures, using them in practice can be a bit clunky:
</p>
<pre class="language-javascript"><code>const map1 = Map({a: 1, b: 2, c: 3})
const map2 = map1.set('b', 50)</code>
</pre>
<p>
Once you start updating large data nested structures, more gnarliness ensues. In introducing a non-native data-type we lose a number of syntactic/typing niceties - it seems that for most people, using new exotic data-types is not worth this hassle.
</p>
<p>
A more recent library that aims to solve these problems is <a href="https://immerjs.github.io/immer/docs/introduction">
immer
</a>
, immer is really neat as it sidesteps the need for new data-types. <em>
I even wrote a <a href="https://github.com/leontrolski/immerframe">
half baked Python port
</a>
of it
</em>
.
</p>
<p>
The example above becomes:
</p>
<pre class="language-javascript"><code>const map1 = { a: 1, b: 2, c: 3 }
const map2 = produce(map1, draft => {
draft.b = 50
})</code>
</pre>
<p>
Immer is a really cool idea and is designed well to work with contemporary frontend patterns, but suffers from two flaws:
</p>
<ul>
<li>
<code class="inline">draft</code>
is a mega magicy proxy object, its website lists a number of <a href="https://immerjs.github.io/immer/docs/pitfalls">
pitfalls
</a>
resulting from this.
</li>
<li>
Immer can only go so far with structural sharing of data.
</li>
</ul>
<details>
<summary>
Some detail on that second point.
</summary>
<p>
With a library that deals in built-in data types, it's only possible to structurally share data that's passed around by reference (basically objects and arrays, not strings or numbers). If we do:
</p>
<pre class="language-javascript"><code>const foo = { some massive nested object }
const array1 = [foo]
const array2 = produce(array1, draft => {
array1.push(42)
})</code>
</pre>
<p>
Then <code class="inline">array2[0]</code>
<em>
is
</em>
<code class="inline">foo</code>
, we didn't have to duplicate anything in memory. Under the hood, <code class="inline">array2</code>
is: some reference to <code class="inline">foo</code>
, and the atomic value <code class="inline">42</code>
.
</p>
<p>
However, if we did:
</p>
<pre class="language-javascript"><code>const array1 = [1, 2, 3, etc, 9999999]
const array2 = produce(array1, draft => {
array1.push(42)
})</code>
</pre>
<p>
Then under the hood <code class="inline">array2</code>
is <em>
not
</em>
a reference to <code class="inline">array2</code>
with <code class="inline">42</code>
tacked on the end, it is a whole new array <code class="inline">[1, 2, 3, etc, 9999999, 42]</code>
.
</p>
<p>
With the kind of data used in most frontend applications, this is not so much of a problem. If however we were writing a library that did involve mucking around with rather long arrays, this is a big problem vis-a-vis memory usage.
</p>
</details>
<p>
As a thought experiment, let's imagine what would happen if we baked the immer concept into the language...
</p>
<h2 id="proposal">
Syntax proposal
</h2>
<p>
The proposed syntax consists of one new keyword, <code class="inline">alter</code>
:
</p>
<pre class="language-javascript"><code>const map1 = { a: 1, b: 2, c: 3 }
const map2 = alter (map1) {
map1.b = 50
}</code>
</pre>
<h2>
The rules
</h2>
<ul>
<li>
Everything we do to mutate <code class="inline">map1</code>
within the <code class="inline">alter</code>
block applies only within the lexical scope of that block.
</li>
<li>
The block evaluates to the final value of <code class="inline">map1</code>
.
</li>
</ul>
<p>
The first rule is important, let's imagine we've added a <code class="inline">--disallow-mutating</code>
flag to node and attempted to run the following:
</p>
<pre class="language-javascript"><code>function nastyMutator(m){
m.b += 1
}
const map1 = { a: 1, b: 2, c: 3 }
const map2 = alter (map1) {
nastyMutator(map1)
}</code>
</pre>
<p>
The interpreter would raise an error for the second line at parse time.
</p>
<em>
Note that we're still allowing reassignment, so the following is permissable:
</em>
<pre class="language-javascript"><code>let map1 = { a: 1, b: 2, c: 3 }
map1 = alter (map1) {
map1.b = 50
}</code>
</pre>
<h3>
Other syntax
</h3>
<p>
The block can just be the next single statement, as with <code class="inline">if</code>
and <code class="inline">for</code>
:
</p>
<pre class="language-javascript"><code>map1 = alter (map1) map1.b = 50</code>
</pre>
<p>
Standard unpacking syntax would enable doing many things at once:
</p>
<pre class="language-javascript"><code>[c, d] = alter ([a, b]) {
c.foo = 1
b.bar = 2
}</code>
</pre>
<h1>
What do we win?
</h1>
<p>
We solve the problems we had with using specialist data types and with immer:
</p>
<ul>
<li>
We can stick to boring objects and arrays and keep all our typing.
</li>
<li>
Within the <code class="inline">alter</code>
block, the variable we specified at the beginning is just a plain 'ol value - no pitfalls resulting from proxy magic.
</li>
<li>
A clever interpreter would be able to do lots of structural sharing, so the following would be memory efficient: <pre class="language-javascript"><code>const array1 = [1, 2, 3, etc, 9999999]
const array2 = alter (array1) {
array1.push(42)
}</code>
</pre>
</li>
</ul>
<p>
Now let's think long term, let's imagine uptake is high - there's no need to install any libraries and converting existing code is easy, so why not - what happens then?
</p>
<p>
We can reach the stage where all the mutating in our codebase is contained within <code class="inline">alter</code>
blocks - we can check this by running with our <code class="inline">--disallow-mutating</code>
flag from earlier. (Maybe we add a <code class="inline">mutate</code>
keyword for specific circumstances so we can do eg: <code class="inline">mutate this.linepos += 1</code>
).
</p>
<p>
Now all our data outside of <code class="inline">alter</code>
blocks is immutable, we can:
</p>
<ul>
<li>
Reason more easily about the code - no mutation at a distance.
</li>
<li>
Do efficient comparisons on stuff, this opens the door for objects in sets and as keys in maps.
</li>
<li>
The interpreter can now do various performance tricks.
</li>
</ul>
<h2>
Application to global-state-styley frontends
</h2>
<p>
As the syntax is lifted straight from immer, we can trivially update <a href="https://immerjs.github.io/immer/docs/example-reducer">
their reducer pattern example
</a>
:
</p>
<pre class="language-javascript"><code>const byId = (state, action) => alter (state) {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
state[product.id] = product
})
}
}</code>
</pre>
<h2>
Python equivalent
</h2>
<p>
The Python version would look the same, just with Python's block syntax:
</p>
<pre class="language-python"><code>const array1 = [1, 2, 3, etc, 9999999]
const array2 = alter array1:
array1.push(42)</code>
</pre>
<details>
<summary>
Aside on semantics of updating deeply nested values in FP languages.
</summary>
<p>
Clojure has the concept of <a href="https://clojure.org/reference/transients">
transients
</a>
that feel somewhat similar to <code class="inline">alter</code>
, I should play around with these more.
</p>
<p>
Haskellers (often it seems) use the <a href="https://hackage.haskell.org/package/lens">
lens
</a>
library for making deep changes to objects. I've seen the Python equivalent used and the resulting code has always been reverted back to a "native" style at some point as it can be tricksy to read and doesn't play well with modern typed Python. An open question for me is:
</p>
<blockquote>
Is there a pure-FP-ish way to doing deep updates that's always as "natural" as the classic mutational way?
</blockquote>
<p>
Here's a contrived example of something that to me "feels natural" in an imperative style, but it could just be my unfamiliarity with FP stuff.
</p>
<pre class="language-javascript"><code>const stateAfter = alter (state) {
const unflagged = []
for (const message of state.messages){
if (message.flagged){
state.flaggedMessages.push(message)
state.totalCount -= 1
delete state.visibleUserIds[message.userId]
}
else unflagged.push(message)
}
state.messages = unflagged
}</code>
</pre>
</details>
</body>
</html>