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

315 lines, 47 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 # I think this is the thing that Go and C# changed!
118 # Gah
119 #
120 # We would have to test multiple blocks in a loop
121 #
122 # for i in (0 .. 3) {
123 # cd /tmp { # this will work
124 # echo $i
125 # }
126 #
127 # var b = ^(echo $i)
128 # call blocks->append(b) # won't work
129 # }
130
131 python3 -c '
132def create_functions():
133 funcs = []
134 for i in range(3):
135 # TODO: This is bad!!! Not idiomatic
136 funcs.append(lambda i=i: i) # Using default argument to capture loop variable
137 #funcs.append(lambda: i)
138 return funcs
139
140functions = create_functions()
141
142for i in range(3):
143 actual = functions[i]()
144 assert i == actual, "%d != %d" % (i, actual)
145
146print(functions[2]())
147 '
148}
149
150nested() {
151 echo 'NESTED JS'
152 echo
153
154 nodejs -e '
155 function outer(x) {
156 return function(y) {
157 return function(z) {
158 return x + y + z;
159 };
160 };
161 }
162 '
163
164 echo 'NESTED PYTHON'
165 echo
166
167 python3 -c '
168def outer(x):
169 def middle(y):
170 def inner(z):
171 return x + y + z
172 return inner
173 return middle
174
175nested = outer(1)(2)
176assert nested(3) == 6, "Test 2 failed"
177 '
178}
179
180value-or-var() {
181 # Good point from HN thread, this doesn't work
182 #
183 # https://news.ycombinator.com/item?id=21095662
184 #
185 # "I think if I were writing a language from scratch, and it included
186 # lambdas, they'd close over values, not variables, and mutating the
187 # closed-over variables would have no effect on the world outside the closure
188 # (or perhaps be disallowed entirely)."
189 #
190 # I think having 'capture' be syntax sugar for value.Obj could do this:
191 #
192 # func f(y) {
193 # var z = {}
194 #
195 # func g(self, x) capture {y, z} -> Int {
196 # return (self.y + x)
197 # }
198 # return (g)
199 # }
200 #
201 # Now you have {y: y, z: z} ==> {__call__: <Func>}
202 #
203 # This would be syntax sugar for:
204 #
205 # func f(y) {
206 # var z = {}
207 #
208 # var attrs = {y, z}
209 # func g(self, x) -> Int {
210 # return (self.y + x)
211 # }
212 # var methods = Object(null, {__call__: g}
213 #
214 # var callable = Object(methods, attrs))
215 # return (callable)
216 # }
217 #
218 # "This mechanism that you suggest about copying values is how Lua used to
219 # work before version 5.0, when they came up with the current upvalue
220 # mechanism"
221 #
222 # I think we could use value.Place if you really want a counter ...
223 #
224 # call counter->setValue(counter.getValue() + 1)
225
226 echo 'VALUE JS'
227 echo
228
229 nodejs -e '
230 var x = 42;
231 var f = function () { return x; }
232 x = 43;
233 var g = function () { return x; }
234
235 console.log(f());
236 console.log(g());
237 '
238
239 # Hm doesn't work
240 echo
241
242 nodejs -e '
243 let x = 42;
244 let f = function () { return x; }
245 x = 43;
246 let g = function () { return x; }
247
248 console.log(f());
249 console.log(g());
250 '
251
252 echo
253 echo 'VALUE PYTHON'
254 echo
255
256 python3 -c '
257x = 42
258f = lambda: x
259x = 43
260g = lambda: x
261
262print(f());
263print(g());
264'
265
266 echo
267 echo 'VALUE LUA'
268 echo
269
270 lua -e '
271local x = 42
272local f = function() return x end
273x = 43
274local g = function() return x end
275
276print(f())
277print(g())
278'
279}
280
281# More against closures:
282#
283# https://news.ycombinator.com/item?id=22110772
284#
285# "I don't understand the intuition of closures and they turn me off to
286# languages immediately. They feel like a hack from someone who didn't want to
287# store a copy of a parent-scope variable within a function."
288#
289# My question, against local scopes (var vs let in ES6) and closures vs.
290# classes:
291#
292# https://news.ycombinator.com/item?id=15225193
293#
294# 1. Modifying collections. map(), filter(), etc. are so much clearer and more
295# declarative than imperatively transforming a collection.
296
297# 2. Callbacks for event handlers or the command pattern. (If you're using a
298# framework that isn't event based, this may not come up much.)
299
300# 3. Wrapping up a bundle of code so that you can defer it, conditionally,
301# execute it, execute it in a certain context, or do stuff before and after it.
302# Python's context stuff handles much of this for you, but then that's another
303# language feature you have to explicitly add.
304
305# Minority opinion about closures:
306#
307# - C# changed closure-in-loop
308# - Go changed closure-in-loop
309# - Lua changed as of 5.0?
310# - TODO: Test out closures in Lua too
311#
312# - Python didn't change it, but people mostly write blog posts about it, and
313# don't hit it?
314
315"$@"