OILS / demo / survey-closure.sh View on Github | oils.pub

569 lines, 81 significant
1#!/usr/bin/env bash
2#
3# Survey closures, with a bunch of comments/notes
4#
5# Usage:
6# demo/survey-closure.sh <function name>
7
8set -o nounset
9set -o pipefail
10set -o errexit
11
12source build/dev-shell.sh # python3 in $PATH
13
14counter() {
15 echo 'COUNTER JS'
16 echo
17
18 nodejs -e '
19 function createCounter() {
20 let count = 0;
21 return function() {
22 // console.log("after", after);
23 count++;
24 return count;
25 };
26 let after = 42;
27 }
28
29 const counter = createCounter();
30 console.assert(counter() === 1, "Test 1.1 failed");
31 console.assert(counter() === 2, "Test 1.2 failed");
32
33 console.log(counter());
34 '
35
36 echo 'COUNTER PYTHON'
37 echo
38
39 python3 -c '
40def create_counter():
41 count = 0
42 def counter():
43 # Python lets you do this!
44 #print("after", after);
45 nonlocal count
46 count += 1
47 return count
48 after = 42
49 return counter
50
51counter = create_counter()
52assert counter() == 1, "Test 1.1 failed"
53assert counter() == 2, "Test 1.2 failed"
54
55print(counter())
56'
57}
58
59# The famous C# / Go issue, and the design note at the end:
60#
61# http://craftinginterpreters.com/closures.html
62#
63# "If a language has a higher-level iterator-based looping structure like
64# foreach in C#, Java’s “enhanced for”, for-of in JavaScript, for-in in Dart,
65# etc., then I think it’s natural to the reader to have each iteration create a
66# new variable. The code looks like a new variable because the loop header
67# looks like a variable declaration."
68#
69# I am Python-minded and I think of it as mutating the same location ...
70#
71# "If you dig around StackOverflow and other places, you find evidence that
72# this is what users expect, because they are very surprised when they don’t
73# get it."
74#
75# I think this depends on which languages they came from
76#
77# JavaScript var vs. let is a good counterpoint ...
78#
79# Another solution for us is to make it explicit:
80#
81# captured var x = 1
82#
83# "The pragmatically useful answer is probably to do what JavaScript does with
84# let in for loops. Make it look like mutation but actually create a new
85# variable each time, because that’s what users want. It is kind of weird when
86# you think about it, though."
87#
88# Ruby has TWO different behaviors, shown there:
89#
90# - for i in 1..2 - this is mutable
91# - (1..2).each do |i| ... - this creates a new variable
92
93loops() {
94 echo 'LOOPS JS'
95 echo
96
97 nodejs -e '
98 function createFunctions() {
99 const funcs = [];
100 for (let i = 0; i < 3; i++) {
101 funcs.push(function() { return i; });
102 }
103 return funcs;
104 }
105
106 const functions = createFunctions();
107 console.assert(functions[0]() === 0, "Test 4.1 failed");
108 console.assert(functions[1]() === 1, "Test 4.2 failed");
109 console.assert(functions[2]() === 2, "Test 4.3 failed");
110
111 console.log(functions[1]())
112 '
113
114 # similar to proc p example
115 echo
116 echo 'JS define function'
117 echo
118
119 nodejs -e '
120 for (let i = 0; i < 5; i++) {
121 // this works, is it like let?
122 function inner() { return i; }
123 // let inner = function() { return i; }
124 }
125 console.log("INNER");
126 console.log(inner());
127 '
128
129 echo
130 echo 'LOOPS PYTHON'
131 echo
132
133 # We would have to test multiple blocks in a loop
134 #
135 # for i in (0 .. 3) {
136 # cd /tmp { # this will work
137 # echo $i
138 # }
139 # }
140
141 python3 -c '
142def create_functions():
143 funcs = []
144 for i in range(3):
145 # TODO: This is bad!!! Not idiomatic
146 funcs.append(lambda i=i: i) # Using default argument to capture loop variable
147 #funcs.append(lambda: i)
148 return funcs
149
150functions = create_functions()
151
152for i in range(3):
153 actual = functions[i]()
154 assert i == actual, "%d != %d" % (i, actual)
155
156print(functions[1]())
157 '
158}
159
160js-while-var() {
161 echo 'WHILE JS'
162 echo
163
164 nodejs -e '
165 function createFunctions() {
166 const funcs = [];
167 let i = 0; // for let is SPECIAL!
168 while (i < 3) {
169 funcs.push(function() { return i; });
170 i++;
171 }
172 return funcs;
173 }
174
175 const functions = createFunctions();
176
177 console.log(functions[0]())
178 console.log(functions[1]())
179 console.log(functions[2]())
180 '
181
182 echo 'FOR VAR JS'
183 echo
184
185 nodejs -e '
186 function createFunctions() {
187 const funcs = [];
188 // var is not captured
189 for (var i = 0; i < 3; i++) {
190 funcs.push(function() { return i; });
191 }
192 return funcs;
193 }
194
195 const functions = createFunctions();
196
197 console.log(functions[0]())
198 console.log(functions[1]())
199 console.log(functions[2]())
200 '
201
202 echo 'FOR LET'
203 echo
204
205 nodejs -e '
206 function createFunctions() {
207 const funcs = [];
208 for (let i = 0; i < 3; i++) {
209 // This is captured
210 // let j = i + 10;
211
212 // This is not captured, I guess it is "hoisted"
213 var j = i + 10;
214 funcs.push(function() { return j; });
215 }
216 return funcs;
217 }
218
219 const functions = createFunctions();
220
221 console.log(functions[0]())
222 console.log(functions[1]())
223 console.log(functions[2]())
224 '
225}
226
227nested() {
228 echo 'NESTED JS'
229 echo
230
231 nodejs -e '
232 function outer(x) {
233 return function(y) {
234 return function(z) {
235 return x + y + z;
236 };
237 };
238 }
239 '
240
241 echo 'NESTED PYTHON'
242 echo
243
244 python3 -c '
245def outer(x):
246 def middle(y):
247 def inner(z):
248 return x + y + z
249 return inner
250 return middle
251
252nested = outer(1)(2)
253assert nested(3) == 6, "Test 2 failed"
254 '
255}
256
257js-mutate-var-let() {
258 nodejs -e '
259 function outer() {
260 var x = "X_outer";
261 var y = "Y_outer";
262
263 function inner() {
264 var x = "X_inner";
265 y = "Y_inner";
266
267 console.log(`inner: x=${x} y=${y}`);
268 }
269
270 inner();
271
272 console.log(`outer: x=${x} y=${y}`);
273 }
274
275 outer();
276 '
277
278 echo
279
280 # Does not change eanything
281 nodejs -e '
282 function outer() {
283 let x = "X_outer";
284 let y = "Y_outer";
285
286 function inner() {
287 let x = "X_inner";
288 y = "Y_inner";
289
290 console.log(`let inner: x=${x} y=${y}`);
291 }
292
293 inner();
294
295 console.log(`let outer: x=${x} y=${y}`);
296 }
297
298 outer();
299 '
300}
301
302value-or-var() {
303 # Good point from HN thread, this doesn't work
304 #
305 # https://news.ycombinator.com/item?id=21095662
306 #
307 # "I think if I were writing a language from scratch, and it included
308 # lambdas, they'd close over values, not variables, and mutating the
309 # closed-over variables would have no effect on the world outside the closure
310 # (or perhaps be disallowed entirely)."
311 #
312 # I think having 'capture' be syntax sugar for value.Obj could do this:
313 #
314 # func f(y) {
315 # var z = {}
316 #
317 # func g(self, x) capture {y, z} -> Int {
318 # return (self.y + x)
319 # }
320 # return (g)
321 # }
322 #
323 # Now you have {y: y, z: z} ==> {__call__: <Func>}
324 #
325 # This would be syntax sugar for:
326 #
327 # func f(y) {
328 # var z = {}
329 #
330 # var attrs = {y, z}
331 # func g(self, x) -> Int {
332 # return (self.y + x)
333 # }
334 # var methods = Object(null, {__call__: g}
335 #
336 # var callable = Object(methods, attrs))
337 # return (callable)
338 # }
339 #
340 # "This mechanism that you suggest about copying values is how Lua used to
341 # work before version 5.0, when they came up with the current upvalue
342 # mechanism"
343 #
344 # I think we could use value.Place if you really want a counter ...
345 #
346 # call counter->setValue(counter.getValue() + 1)
347
348 echo 'VALUE JS'
349 echo
350
351 nodejs -e '
352 var x = 42;
353 var f = function () { return x; }
354 x = 43;
355 var g = function () { return x; }
356
357 console.log(f());
358 console.log(g());
359 '
360
361 # Hm doesn't work
362 echo
363
364 nodejs -e '
365 let x = 42;
366 let f = function () { return x; }
367 x = 43;
368 let g = function () { return x; }
369
370 console.log(f());
371 console.log(g());
372 '
373
374 echo
375 echo 'VALUE PYTHON'
376 echo
377
378 python3 -c '
379x = 42
380f = lambda: x
381x = 43
382g = lambda: x
383
384print(f());
385print(g());
386'
387
388 echo
389 echo 'VALUE LUA'
390 echo
391
392 lua -e '
393local x = 42
394local f = function() return x end
395x = 43
396local g = function() return x end
397
398print(f())
399print(g())
400'
401}
402
403# More against closures:
404#
405# https://news.ycombinator.com/item?id=22110772
406#
407# "I don't understand the intuition of closures and they turn me off to
408# languages immediately. They feel like a hack from someone who didn't want to
409# store a copy of a parent-scope variable within a function."
410#
411# My question, against local scopes (var vs let in ES6) and closures vs.
412# classes:
413#
414# https://news.ycombinator.com/item?id=15225193
415#
416# 1. Modifying collections. map(), filter(), etc. are so much clearer and more
417# declarative than imperatively transforming a collection.
418
419# 2. Callbacks for event handlers or the command pattern. (If you're using a
420# framework that isn't event based, this may not come up much.)
421
422# 3. Wrapping up a bundle of code so that you can defer it, conditionally,
423# execute it, execute it in a certain context, or do stuff before and after it.
424# Python's context stuff handles much of this for you, but then that's another
425# language feature you have to explicitly add.
426
427# Minority opinion about closures:
428#
429# - C# changed closure-in-loop
430# - Go changed closure-in-loop
431# - Lua changed as of 5.0?
432# - TODO: Test out closures in Lua too
433#
434# - Python didn't change it, but people mostly write blog posts about it, and
435# don't hit it?
436
437
438ruby-blocks() {
439 ruby -e '
440def create_multiplier(factor)
441 ->(x) { x * factor }
442end
443
444double = create_multiplier(2)
445triple = create_multiplier(3)
446
447puts double.call(5) # Output: 10
448puts triple.call(5) # Output: 15
449'
450 echo
451
452 ruby -e '
453def use_multiplier(factor)
454 # This method yields to a block
455 yield factor
456end
457
458multiplier = 3
459
460# The block captures the outer multiplier variable
461result = use_multiplier(5) { |x| x * multiplier }
462puts result # Output: 15
463
464# alternative syntax
465result = use_multiplier(5) do |x|
466 x * multiplier
467end
468
469puts result # Output: 15
470'
471 echo
472
473 ruby -e '
474# alternative syntax
475def use_multiplier(factor, &block)
476 block.call(factor)
477end
478
479multiplier = 3
480
481result = use_multiplier(5) { |x| x * multiplier }
482puts result # Output: 15
483
484# alterantive syntax
485result = use_multiplier(5) do |x|
486 x * multiplier
487end
488
489puts result # Output: 15
490'
491}
492
493ruby-mine() {
494 ruby -e '
495# Two styles
496
497# Implicit block arg
498def run_it
499 yield 2
500end
501
502# explicit proc arg
503def run_it2 (&block) # interchangeable
504 block.call(2)
505end
506
507# 2 Styles of Block
508
509factor = 3
510
511block1 = ->(x) { x * factor }
512puts block1.call(5)
513
514result = run_it(&block1)
515puts result
516
517puts
518
519g = 9 # visible
520
521block2 = lambda do |x|
522 x * factor * g
523 h = 20
524end
525
526# Not visible, but in YSH we may want it to be, e.g. for try { } and shopt { }
527# puts h
528
529puts block2.call(5)
530
531result = run_it(&block2)
532puts result
533
534puts
535
536# 2 styles of Proc
537
538proc1 = proc { |x| x * factor }
539puts proc1.call(5)
540
541result = run_it(&proc1)
542puts result
543
544puts
545
546proc2 = Proc.new do |x|
547 x * factor
548end
549puts proc2.call(5)
550
551result = run_it(&proc2)
552puts result
553
554puts
555
556# Now do a literal style
557
558result = run_it do |x|
559 x * factor
560end
561puts result
562'
563}
564
565ruby-binding() {
566 ruby demo/survey-closure.rb
567}
568
569"$@"