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

524 lines, 76 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
257value-or-var() {
258 # Good point from HN thread, this doesn't work
259 #
260 # https://news.ycombinator.com/item?id=21095662
261 #
262 # "I think if I were writing a language from scratch, and it included
263 # lambdas, they'd close over values, not variables, and mutating the
264 # closed-over variables would have no effect on the world outside the closure
265 # (or perhaps be disallowed entirely)."
266 #
267 # I think having 'capture' be syntax sugar for value.Obj could do this:
268 #
269 # func f(y) {
270 # var z = {}
271 #
272 # func g(self, x) capture {y, z} -> Int {
273 # return (self.y + x)
274 # }
275 # return (g)
276 # }
277 #
278 # Now you have {y: y, z: z} ==> {__call__: <Func>}
279 #
280 # This would be syntax sugar for:
281 #
282 # func f(y) {
283 # var z = {}
284 #
285 # var attrs = {y, z}
286 # func g(self, x) -> Int {
287 # return (self.y + x)
288 # }
289 # var methods = Object(null, {__call__: g}
290 #
291 # var callable = Object(methods, attrs))
292 # return (callable)
293 # }
294 #
295 # "This mechanism that you suggest about copying values is how Lua used to
296 # work before version 5.0, when they came up with the current upvalue
297 # mechanism"
298 #
299 # I think we could use value.Place if you really want a counter ...
300 #
301 # call counter->setValue(counter.getValue() + 1)
302
303 echo 'VALUE JS'
304 echo
305
306 nodejs -e '
307 var x = 42;
308 var f = function () { return x; }
309 x = 43;
310 var g = function () { return x; }
311
312 console.log(f());
313 console.log(g());
314 '
315
316 # Hm doesn't work
317 echo
318
319 nodejs -e '
320 let x = 42;
321 let f = function () { return x; }
322 x = 43;
323 let g = function () { return x; }
324
325 console.log(f());
326 console.log(g());
327 '
328
329 echo
330 echo 'VALUE PYTHON'
331 echo
332
333 python3 -c '
334x = 42
335f = lambda: x
336x = 43
337g = lambda: x
338
339print(f());
340print(g());
341'
342
343 echo
344 echo 'VALUE LUA'
345 echo
346
347 lua -e '
348local x = 42
349local f = function() return x end
350x = 43
351local g = function() return x end
352
353print(f())
354print(g())
355'
356}
357
358# More against closures:
359#
360# https://news.ycombinator.com/item?id=22110772
361#
362# "I don't understand the intuition of closures and they turn me off to
363# languages immediately. They feel like a hack from someone who didn't want to
364# store a copy of a parent-scope variable within a function."
365#
366# My question, against local scopes (var vs let in ES6) and closures vs.
367# classes:
368#
369# https://news.ycombinator.com/item?id=15225193
370#
371# 1. Modifying collections. map(), filter(), etc. are so much clearer and more
372# declarative than imperatively transforming a collection.
373
374# 2. Callbacks for event handlers or the command pattern. (If you're using a
375# framework that isn't event based, this may not come up much.)
376
377# 3. Wrapping up a bundle of code so that you can defer it, conditionally,
378# execute it, execute it in a certain context, or do stuff before and after it.
379# Python's context stuff handles much of this for you, but then that's another
380# language feature you have to explicitly add.
381
382# Minority opinion about closures:
383#
384# - C# changed closure-in-loop
385# - Go changed closure-in-loop
386# - Lua changed as of 5.0?
387# - TODO: Test out closures in Lua too
388#
389# - Python didn't change it, but people mostly write blog posts about it, and
390# don't hit it?
391
392
393ruby-blocks() {
394 ruby -e '
395def create_multiplier(factor)
396 ->(x) { x * factor }
397end
398
399double = create_multiplier(2)
400triple = create_multiplier(3)
401
402puts double.call(5) # Output: 10
403puts triple.call(5) # Output: 15
404'
405 echo
406
407 ruby -e '
408def use_multiplier(factor)
409 # This method yields to a block
410 yield factor
411end
412
413multiplier = 3
414
415# The block captures the outer multiplier variable
416result = use_multiplier(5) { |x| x * multiplier }
417puts result # Output: 15
418
419# alternative syntax
420result = use_multiplier(5) do |x|
421 x * multiplier
422end
423
424puts result # Output: 15
425'
426 echo
427
428 ruby -e '
429# alternative syntax
430def use_multiplier(factor, &block)
431 block.call(factor)
432end
433
434multiplier = 3
435
436result = use_multiplier(5) { |x| x * multiplier }
437puts result # Output: 15
438
439# alterantive syntax
440result = use_multiplier(5) do |x|
441 x * multiplier
442end
443
444puts result # Output: 15
445'
446}
447
448ruby-mine() {
449 ruby -e '
450# Two styles
451
452# Implicit block arg
453def run_it
454 yield 2
455end
456
457# explicit proc arg
458def run_it2 (&block) # interchangeable
459 block.call(2)
460end
461
462# 2 Styles of Block
463
464factor = 3
465
466block1 = ->(x) { x * factor }
467puts block1.call(5)
468
469result = run_it(&block1)
470puts result
471
472puts
473
474g = 9 # visible
475
476block2 = lambda do |x|
477 x * factor * g
478 h = 20
479end
480
481# Not visible, but in YSH we may want it to be, e.g. for try { } and shopt { }
482# puts h
483
484puts block2.call(5)
485
486result = run_it(&block2)
487puts result
488
489puts
490
491# 2 styles of Proc
492
493proc1 = proc { |x| x * factor }
494puts proc1.call(5)
495
496result = run_it(&proc1)
497puts result
498
499puts
500
501proc2 = Proc.new do |x|
502 x * factor
503end
504puts proc2.call(5)
505
506result = run_it(&proc2)
507puts result
508
509puts
510
511# Now do a literal style
512
513result = run_it do |x|
514 x * factor
515end
516puts result
517'
518}
519
520ruby-binding() {
521 ruby demo/survey-closure.rb
522}
523
524"$@"