@@ -63,11 +63,13 @@ fn add(val) {
63
63
When we reference the ** l** variable it uses the reference on the
64
64
outer scope (the empty list), but there is no way to express syntactically
65
65
that we want to change the list on the outer scope instead of creating
66
- a new variable ** l** . That is why the ** get** and ** print** functions
66
+ a new variable ** l** (shadowing the outer ** l** ).
67
+
68
+ That is why the ** get** and ** print** functions
67
69
are always referencing an outer list ** l** that is empty, a new one
68
70
is created each time the add function is called.
69
71
70
- In this document we brainstorm about possible solutions to this.
72
+ In this document we navigate the solution space for this problem .
71
73
72
74
## Proposal I - Create new variables explicitly
73
75
@@ -88,11 +90,10 @@ i = "0"
88
90
```
89
91
90
92
Will be assigning a new value to an already existent variable ** i** .
91
-
92
- The assignment must first look for the target variable in the local
93
- scope and then in the parent, recursively, until it's found and then updated,
94
- otherwise (in case the variable is not found) the interpreter must abort
95
- with error.
93
+ The assignment will first look for the target variable in the local
94
+ scope and then in the parent, traversing the entire stack, until it's
95
+ found and then updated, otherwise (in case the variable is not found)
96
+ the interpreter must abort with error.
96
97
97
98
``` sh
98
99
var count = " 0" # declare local variable
@@ -106,8 +107,7 @@ inc()
106
107
print($count ) # outputs: 1
107
108
```
108
109
109
- Below is how this proposal solves the
110
- scope management problem example:
110
+ Below is how this proposal solves the list example:
111
111
112
112
``` sh
113
113
fn list () {
@@ -153,67 +153,117 @@ var body, err <= curl -f $url
153
153
var name, surname, err < = getAuthor ()
154
154
```
155
155
156
- One of the downsides of ` var ` is the requirement that none of the
157
- targeted variable exists, because it makes awkward when existent
158
- variables must be used in conjunction with new ones. An example is the
159
- variables ` $status ` and ` $err ` that are often used to get process exit
160
- status and errors from functions, respectively.
161
-
162
- The [ PR #227 ] ( https://github.com/NeowayLabs/nash/pull/227 ) implements
163
- this proposal but deviates in multiple assignments to handle the
164
- downside above. The ` var ` statement was implemented with the rules
165
- below:
166
-
167
- 1 . At least one of the targeted variables must do not exists;
168
- 2 . The existent variables are just updated in the scope it resides;
156
+ Using var always creates new variables, shadowing previous ones,
157
+ for example:
169
158
170
- Below are some valid examples with [ #227 ] ( https://github.com/NeowayLabs/nash/pull/227 ) :
171
159
172
160
``` sh
173
161
var a, b = " 0" , " 1" # works fine, variables didn't existed before
174
162
175
- var a, b = " 2" , " 3" # error by rule 1
163
+ var a, b, c = " 4" , " 5" , " 6" # works! too, creating new a, b, c
164
+ ```
165
+
166
+ On a dynamic typed language there is very little difference between
167
+ creating a new var or just reassigning it since variables are just
168
+ references that store no type information at all. For example,
169
+ what is the difference between this:
176
170
177
- # works! c is declared but 'a' and 'b' are updated (by rule 2)
178
- var a, b, c = " 4" , " 5" , " 6"
171
+ ```
172
+ var a = "1"
173
+ a = ()
174
+ ```
179
175
180
- # works, variables first declared
181
- var users, err < = cat /etc/passwd | awk -F " :" " {print $1 }"
176
+ And this ?
182
177
183
- # also works, but $err just updated
184
- var pass, err < = cat /etc/shadow | awk -F " :" " {print $2 }"
178
+ ```
179
+ var a = "1"
180
+ var a = ()
185
181
```
186
182
187
- The implementation above is handy but makes the meaning of ` var `
188
- confuse because it declares new variables ** and** update existent ones
189
- (in outer scopes also). Then making hard to know what variables are
190
- being declared local and what are being updated, by just looking at
191
- the statement, because the meaning will depend in the current
192
- environment of variables.
183
+ The behavior will be exactly the same, there is no semantic error
184
+ on reassigning the same variable to a value with a different type,
185
+ so reassigning on redeclaring has no difference at all (although it
186
+ makes sense for statically typed languages).
193
187
194
- Another downside of ` var ` is their very incompatible nature. Every
195
- nash script ever created will be affected.
188
+ Statements are evaluated in order, so this:
196
189
197
- Another behavior we need to discuss is whether all variables declared
198
- within a function (statement) will be created at the beginning
199
- (as scope is created) or only at the time a 'var' keywork is found.
190
+ ```
191
+ a = ()
192
+ var a = "1"
193
+ ```
200
194
201
- ``` sh
195
+ Is ** NOT** the same as this:
196
+
197
+ ```
198
+ var a = "1"
199
+ var a = ()
200
+ ```
201
+
202
+ This is easier to understand when using closures, let's go
203
+ back to our list implementation, we had something like this:
204
+
205
+ ```
202
206
var l = ()
203
207
204
- fn test () {
205
- l < = append($l , " 1" )
206
- var l = ()
208
+ fn add(val) {
209
+ # use the "l" variable from parent scope
210
+ # find first in the this scope if not found
211
+ # then find variable in the parent scope
212
+ l <= append($l, $val)
213
+ }
214
+ ```
215
+
216
+ If we write this:
217
+
218
+ ```
219
+ var l = ()
220
+
221
+ fn add(val) {
222
+ # creates new var
223
+ var l = ()
224
+ # manipulates new l var
225
+ l <= append($l, $val)
207
226
}
227
+ ```
208
228
209
- print($l ) # outputs: "1"
229
+ The ** add** function will not manipulate the ** l** variable from the
230
+ outer scope, and our list implementation will not work properly.
231
+
232
+ But writing this:
233
+
234
+ ```
235
+ var l = ()
236
+
237
+ fn add(val) {
238
+ # manipulates outer l var
239
+ l <= append($l, $val)
240
+ # creates new var that is useless
241
+ var l = ()
242
+ }
210
243
```
211
244
212
- ## Proposal II - "outer"
245
+ Will work, since we assigned a new value to the outer ** l**
246
+ before creating a new ** l** var.
247
+
248
+ The approach described here is very similar to how variables
249
+ are handled in [ Lua] ( https://www.lua.org/ ) , with the exception
250
+ that Lua uses the ** local** keyword, instead of var.
251
+
252
+ Also, Lua allows global variables to be created by default, on
253
+ Nash we prefer to avoid global stuff and produce an error when
254
+ assigning new values to variables that do not exist.
255
+
256
+ Summarizing, on this proposal creating new variables is explicit
257
+ and referencing existent variables on outer scopes is implicit.
258
+
259
+
260
+ ## Proposal II - Manipulate outer scope explicitly
213
261
214
262
This proposal adds a new ` outer ` keyword that permits the update of
215
- variables in the outer scope. Outer assignments with non-existent
216
- variables is an error.
263
+ variables in the outer scope. The default and implicit behavior of
264
+ variable assignments is to always create a new variable.
265
+
266
+ Considering our list example:
217
267
218
268
``` sh
219
269
fn list () {
@@ -235,15 +285,158 @@ fn list() {
235
285
print(" list: [%s]\n" , $l )
236
286
}
237
287
238
- fn not_clear () {
239
- # "l" is not cleared, but a new a new variable is created (shadowing)
240
- # because "outer" isn't specified.
241
- l = ()
242
- }
243
-
244
288
return $add , $get , $string
245
289
}
246
290
```
247
291
248
292
The ` outer ` keyword has the same meaning that Python's ` global `
249
293
keyword.
294
+
295
+ Different from Python global, outer must appear on all assignments,
296
+ like this:
297
+
298
+ ``` sh
299
+ fn list () {
300
+ # initialize an "l" variable in this scope
301
+ l = ()
302
+
303
+ fn doubleadd(val) {
304
+ outer l < = append($l , $val )
305
+ outer l < = append($l , $val )
306
+ }
307
+
308
+ return $doubleadd
309
+ }
310
+ ```
311
+
312
+ This would be buggy and only add once:
313
+
314
+ ``` sh
315
+ fn list () {
316
+ # initialize an "l" variable in this scope
317
+ l = ()
318
+
319
+ fn doubleadd(val) {
320
+ outer l < = append($l , $val )
321
+ l < = append($l , $val )
322
+ }
323
+
324
+ return $doubleadd
325
+ }
326
+ ```
327
+
328
+ Trying to elaborate more on possible combinations
329
+ when using the ** outer** keyword we get at some hard
330
+ questions, like what does outer means on this case:
331
+
332
+ ```
333
+ fn list() {
334
+ # initialize an "l" variable in this scope
335
+ l = ()
336
+ fn doubleadd(val) {
337
+ l <= append($l, $val)
338
+ outer l <= append($l, $val)
339
+ }
340
+ return $doubleadd
341
+ }
342
+ ```
343
+
344
+ Will outer just handle the reference on its own scope or
345
+ will it jump its own scope and manipulate the outer variable ?
346
+
347
+ The name outer implies that it will manipulate the outer scope,
348
+ bypassing its own current scope, but how do you read the outer
349
+ variable ? We would need to support something like:
350
+
351
+ ```
352
+ fn list() {
353
+ # initialize an "l" variable in this scope
354
+ l = ()
355
+ fn add(val) {
356
+ l <= "whatever"
357
+ outer l <= append(outer $l, $val)
358
+ }
359
+ return $doubleadd
360
+ }
361
+ ```
362
+
363
+ It is like with outer we are bypassing the lexical semantics
364
+ of the code, the order of declarations is not relevant anymore
365
+ since you have a form of "goto" to jump the current scope.
366
+
367
+ ## Comparing both approaches
368
+
369
+ As everything in life, the design space for how to handle
370
+ scope management is full of tradeoffs.
371
+
372
+ Making outer scope management explicit makes declaring
373
+ new variables easier, since you have to type less to
374
+ create new vars.
375
+
376
+ But managing scope using closures gets more cumbersome,
377
+ consider this nested closures with the ** outer** keyword:
378
+
379
+ ``` sh
380
+ fn list () {
381
+ l = ()
382
+
383
+ fn add(val) {
384
+ # use the "l" variable from the parent
385
+ outer l < = append($l , $val )
386
+ fn addagain () {
387
+ outer l < = append($l , $val )
388
+ }
389
+ return $addagain
390
+ }
391
+
392
+ return $add
393
+ }
394
+ ```
395
+
396
+ And this one with ** var** :
397
+
398
+ ``` sh
399
+ fn list () {
400
+ var l = ()
401
+
402
+ fn add(val) {
403
+ # use the "l" variable from the parent
404
+ l < = append($l , $val )
405
+ fn addagain () {
406
+ l < = append($l , $val )
407
+ }
408
+ return $addagain
409
+ }
410
+
411
+ return $add
412
+ }
413
+ ```
414
+
415
+ The ** var** option requires more writing for the common
416
+ case of declaring new variables (specially on the interactive shell
417
+ this is pretty annoying), but makes closures pretty
418
+ natural to write, you just manipulate the variables
419
+ that exists lexically on your scope, like you would do
420
+ inside a ** if** or ** for** block.
421
+
422
+ Thinking about cognition, it seems easier to write buggy code
423
+ by forgetting to add an ** outer** on the code than forgetting
424
+ to add a ** var** and by mistake manipulate an variable outside
425
+ the scope.
426
+
427
+ The decision to break if the variable does not exist also enhances
428
+ the ** var** option as less buggy since no new variable will be
429
+ created if you forget the ** var** , but lexically reachable variables
430
+ will be manipulated (this is ameliorated by the fact that we don't have
431
+ global variables).
432
+
433
+ If we go for ** outer** it seems that we are going to write less,
434
+ but some code, involving closures, will be harder to read (and write).
435
+ Since code is usually read more than it is written it seems like a sensible
436
+ choice to optimize for readability and understandability than just
437
+ save a few keystrokes.
438
+
439
+ But any statements made about cognition are really hard to be
440
+ considered as a global truth, since all human beings are biased which makes
441
+ identification of common patterns of cognition really hard. But if software
442
+ design has any kind of goal, must be this =).
0 commit comments