OILS / demo / survey-closure.sh View on Github | oilshell.org

508 lines, 71 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[2]())
112 '
113
114 echo 'LOOPS PYTHON'
115 echo
116
117 # We would have to test multiple blocks in a loop
118 #
119 # for i in (0 .. 3) {
120 # cd /tmp { # this will work
121 # echo $i
122 # }
123 # }
124
125 python3 -c '
126def create_functions():
127 funcs = []
128 for i in range(3):
129 # TODO: This is bad!!! Not idiomatic
130 funcs.append(lambda i=i: i) # Using default argument to capture loop variable
131 #funcs.append(lambda: i)
132 return funcs
133
134functions = create_functions()
135
136for i in range(3):
137 actual = functions[i]()
138 assert i == actual, "%d != %d" % (i, actual)
139
140print(functions[2]())
141 '
142}
143
144js-while-var() {
145 echo 'WHILE JS'
146 echo
147
148 nodejs -e '
149 function createFunctions() {
150 const funcs = [];
151 let i = 0; // for let is SPECIAL!
152 while (i < 3) {
153 funcs.push(function() { return i; });
154 i++;
155 }
156 return funcs;
157 }
158
159 const functions = createFunctions();
160
161 console.log(functions[0]())
162 console.log(functions[1]())
163 console.log(functions[2]())
164 '
165
166 echo 'FOR VAR JS'
167 echo
168
169 nodejs -e '
170 function createFunctions() {
171 const funcs = [];
172 // var is not captured
173 for (var i = 0; i < 3; i++) {
174 funcs.push(function() { return i; });
175 }
176 return funcs;
177 }
178
179 const functions = createFunctions();
180
181 console.log(functions[0]())
182 console.log(functions[1]())
183 console.log(functions[2]())
184 '
185
186 echo 'FOR LET'
187 echo
188
189 nodejs -e '
190 function createFunctions() {
191 const funcs = [];
192 for (let i = 0; i < 3; i++) {
193 // This is captured
194 // let j = i + 10;
195
196 // This is not captured, I guess it is "hoisted"
197 var j = i + 10;
198 funcs.push(function() { return j; });
199 }
200 return funcs;
201 }
202
203 const functions = createFunctions();
204
205 console.log(functions[0]())
206 console.log(functions[1]())
207 console.log(functions[2]())
208 '
209}
210
211nested() {
212 echo 'NESTED JS'
213 echo
214
215 nodejs -e '
216 function outer(x) {
217 return function(y) {
218 return function(z) {
219 return x + y + z;
220 };
221 };
222 }
223 '
224
225 echo 'NESTED PYTHON'
226 echo
227
228 python3 -c '
229def outer(x):
230 def middle(y):
231 def inner(z):
232 return x + y + z
233 return inner
234 return middle
235
236nested = outer(1)(2)
237assert nested(3) == 6, "Test 2 failed"
238 '
239}
240
241value-or-var() {
242 # Good point from HN thread, this doesn't work
243 #
244 # https://news.ycombinator.com/item?id=21095662
245 #
246 # "I think if I were writing a language from scratch, and it included
247 # lambdas, they'd close over values, not variables, and mutating the
248 # closed-over variables would have no effect on the world outside the closure
249 # (or perhaps be disallowed entirely)."
250 #
251 # I think having 'capture' be syntax sugar for value.Obj could do this:
252 #
253 # func f(y) {
254 # var z = {}
255 #
256 # func g(self, x) capture {y, z} -> Int {
257 # return (self.y + x)
258 # }
259 # return (g)
260 # }
261 #
262 # Now you have {y: y, z: z} ==> {__call__: <Func>}
263 #
264 # This would be syntax sugar for:
265 #
266 # func f(y) {
267 # var z = {}
268 #
269 # var attrs = {y, z}
270 # func g(self, x) -> Int {
271 # return (self.y + x)
272 # }
273 # var methods = Object(null, {__call__: g}
274 #
275 # var callable = Object(methods, attrs))
276 # return (callable)
277 # }
278 #
279 # "This mechanism that you suggest about copying values is how Lua used to
280 # work before version 5.0, when they came up with the current upvalue
281 # mechanism"
282 #
283 # I think we could use value.Place if you really want a counter ...
284 #
285 # call counter->setValue(counter.getValue() + 1)
286
287 echo 'VALUE JS'
288 echo
289
290 nodejs -e '
291 var x = 42;
292 var f = function () { return x; }
293 x = 43;
294 var g = function () { return x; }
295
296 console.log(f());
297 console.log(g());
298 '
299
300 # Hm doesn't work
301 echo
302
303 nodejs -e '
304 let x = 42;
305 let f = function () { return x; }
306 x = 43;
307 let g = function () { return x; }
308
309 console.log(f());
310 console.log(g());
311 '
312
313 echo
314 echo 'VALUE PYTHON'
315 echo
316
317 python3 -c '
318x = 42
319f = lambda: x
320x = 43
321g = lambda: x
322
323print(f());
324print(g());
325'
326
327 echo
328 echo 'VALUE LUA'
329 echo
330
331 lua -e '
332local x = 42
333local f = function() return x end
334x = 43
335local g = function() return x end
336
337print(f())
338print(g())
339'
340}
341
342# More against closures:
343#
344# https://news.ycombinator.com/item?id=22110772
345#
346# "I don't understand the intuition of closures and they turn me off to
347# languages immediately. They feel like a hack from someone who didn't want to
348# store a copy of a parent-scope variable within a function."
349#
350# My question, against local scopes (var vs let in ES6) and closures vs.
351# classes:
352#
353# https://news.ycombinator.com/item?id=15225193
354#
355# 1. Modifying collections. map(), filter(), etc. are so much clearer and more
356# declarative than imperatively transforming a collection.
357
358# 2. Callbacks for event handlers or the command pattern. (If you're using a
359# framework that isn't event based, this may not come up much.)
360
361# 3. Wrapping up a bundle of code so that you can defer it, conditionally,
362# execute it, execute it in a certain context, or do stuff before and after it.
363# Python's context stuff handles much of this for you, but then that's another
364# language feature you have to explicitly add.
365
366# Minority opinion about closures:
367#
368# - C# changed closure-in-loop
369# - Go changed closure-in-loop
370# - Lua changed as of 5.0?
371# - TODO: Test out closures in Lua too
372#
373# - Python didn't change it, but people mostly write blog posts about it, and
374# don't hit it?
375
376
377ruby-blocks() {
378 ruby -e '
379def create_multiplier(factor)
380 ->(x) { x * factor }
381end
382
383double = create_multiplier(2)
384triple = create_multiplier(3)
385
386puts double.call(5) # Output: 10
387puts triple.call(5) # Output: 15
388'
389 echo
390
391 ruby -e '
392def use_multiplier(factor)
393 # This method yields to a block
394 yield factor
395end
396
397multiplier = 3
398
399# The block captures the outer multiplier variable
400result = use_multiplier(5) { |x| x * multiplier }
401puts result # Output: 15
402
403# alternative syntax
404result = use_multiplier(5) do |x|
405 x * multiplier
406end
407
408puts result # Output: 15
409'
410 echo
411
412 ruby -e '
413# alternative syntax
414def use_multiplier(factor, &block)
415 block.call(factor)
416end
417
418multiplier = 3
419
420result = use_multiplier(5) { |x| x * multiplier }
421puts result # Output: 15
422
423# alterantive syntax
424result = use_multiplier(5) do |x|
425 x * multiplier
426end
427
428puts result # Output: 15
429'
430}
431
432ruby-mine() {
433 ruby -e '
434# Two styles
435
436# Implicit block arg
437def run_it
438 yield 2
439end
440
441# explicit proc arg
442def run_it2 (&block) # interchangeable
443 block.call(2)
444end
445
446# 2 Styles of Block
447
448factor = 3
449
450block1 = ->(x) { x * factor }
451puts block1.call(5)
452
453result = run_it(&block1)
454puts result
455
456puts
457
458g = 9 # visible
459
460block2 = lambda do |x|
461 x * factor * g
462 h = 20
463end
464
465# Not visible, but in YSH we may want it to be, e.g. for try { } and shopt { }
466# puts h
467
468puts block2.call(5)
469
470result = run_it(&block2)
471puts result
472
473puts
474
475# 2 styles of Proc
476
477proc1 = proc { |x| x * factor }
478puts proc1.call(5)
479
480result = run_it(&proc1)
481puts result
482
483puts
484
485proc2 = Proc.new do |x|
486 x * factor
487end
488puts proc2.call(5)
489
490result = run_it(&proc2)
491puts result
492
493puts
494
495# Now do a literal style
496
497result = run_it do |x|
498 x * factor
499end
500puts result
501'
502}
503
504ruby-binding() {
505 ruby demo/survey-closure.rb
506}
507
508"$@"