-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathpoor-mans-object.html
More file actions
313 lines (276 loc) · 10.5 KB
/
Copy pathpoor-mans-object.html
File metadata and controls
313 lines (276 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<title>leontrolski - Poor man's object</title>
<style>
body {margin: 5% auto; background: #fff7f7; color: #444444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.8;max-width: 63%;}
@media screen and (max-width: 800px) {body {font-size: 14px; line-height: 1.4; max-width: 90%;}}
pre {width: 100%; border-top: 3px solid gray; border-bottom: 3px solid gray;}
a {border-bottom: 1px solid #444444; color: #444444; text-decoration: none;text-shadow: 0 1px 0 #ffffff;}
a:hover {border-bottom: 0;}
</style>
<link href="https://unpkg.com/prism-themes@1.9.0/themes/prism-darcula.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.20.0/plugins/autoloader/prism-autoloader.min.js"></script>
</head>
<body class="language-javascript">
<a href="index.html"><img style="height:2em" src="pic.png"/>⇦</a>
<p><i>2020-03-30</i></p>
<h1>Make python-style classes from not much javascript</h1>
<p>
It's been said many times before, <a href="https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent">objects are a poor man's closures are a poor man's objects</a>, this is a <em>deep and important thing</em>. In python and javascript, the leap is really quite small. I thought I'd try drum the lesson home by implementing a limited bit of the python object system with a subset of javascript. We will:
<ul>
<li>Describe a small subset of javascript with strings, arrays and functions.</li>
<li>Show how with a bit of syntactic sugar you can add dicts (AKA maps, AKA plain ol' objects) to the above.</li>
<li>Use closures to bind functions to a <code>self</code>.</li>
<li>Wrap that all up into a function <code>ClassMaker</code>, that if you squint, is just like the python class syntax.</li>
<li>In a bonus section, implement inheritance.</li>
</ul>
</p>
<b>
You'll have the best time with this post if you play around with the more complicated code snippets in the <a href="https://javascript.info/devtools">developer tools console <code>Cmd + Opt + J or F12</code></a>.
</b>
<h2>Our aim</h2>
<p>
Our aim is to start with a very small language and write some code such that we can make classes similar to how we would in python, and have them behave similarly too:
</p>
<pre id="aim" class="language-python"><code>class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return "My name is " + self.name + " and I am " + self.age + " years old"
>>> const daisy = Animal("Daisy", "32")
>>> daisy.age
"32"
>>> daisy.greet()
"My name is Daisy and I am 32 years old"</pre></code>
<h2>
Our subset of javascript
</h2>
<p>
We're going to pick a small bit of javascript to use, this bit may seem obvious, but we're just demonstrating how little stuff we need to get to OO. Feel free to go quickly through this bit.
</p>
<p>Values are numbers, strings or arrays of values:</p>
<pre><code>3.14 "string" [value]</pre></code>
<p>We can assign values to names:</p>
<pre><code>const a = "foo"</pre></code>
<p>We can get the nth element of an array with:</p>
<pre><code>array[n]</pre></code>
<p>We can concatenate strings/add numbers:</p>
<pre><code>>>> "hello " + "oli"
"hello oli"
>>> 2 + 3
5</pre></code>
<p>We can loop over arrays:</p>
<pre><code>for (const n of l){do something}</pre></code>
<p>If two strings are equal, we can do something:</p>
<pre><code>if (a === b){do something}</pre></code>
<p>We have functions that take arguments and return something:</p>
<pre><code>(x, y) => {return z}</pre></code>
<p>We can refer to all the arguments of a function as an array:</p>
<pre><code>(...args) => {return args}</pre></code>
<p>Functions are values too, we can pass them around:</p>
<pre><code>const addFour = (a) => {return a + 4}
const addNine = (a) => {return a + 9}
const adders = [addFour, addNine]
>>> adders[0](10)
14
>>> adders[1](10)
19</pre></code>
<p>
Assignments within the curly brackets of a function are visible to inner functions within the same curly brackets (lexical scope). These values are available for the lifetime of the inner function (closures). For example:
</p>
<pre><code>const makeAdder = (b) => {
const adder = (a) => {
return a + b
}
return adder
}
const addThree = makeAdder(3)
const addSeven = makeAdder(7)
>>> addThree(5)
8
>>> addSeven(6)
13</pre></code>
<h2>Implementing dicts</h2>
<p>Now let's make something slightly more useful:</p>
<pre><code>const makeDict = (l) => {
const getter = (getk) => {
// for each [key, value] tuple in l,
// if the key matches getK, we return the value
for (const kv of l){
if (kv[0] === getk){
return kv[1]
}
}
}
return getter
}
const d = makeDict([
["foo", "1"],
["bar", "2"],
["baz", "3"],
])
>>> d("bar")
"2"</pre></code>
<p>
OK, looks familiar, now let's imagine that we are going to use this <code>makeDict(...)</code> construct again and again. The program that runs our javascript (the interpreter) is going to transform a cuter bit of syntax (shown below) into our <code>makeDict(...)</code> code (shown above) before it runs it (syntactic sugar):
</p>
<pre><code>const d = {
"foo": "1",
"bar": "2",
"baz": "3",
}
>>> d["bar"]
"2"</pre></code>
<p>
That's a bit better, but let's make the transformation even cuter:
</p>
<pre><code>const d = {
foo: "1",
bar: "2",
baz: "3",
}
>>> d.bar
"2"</pre></code>
<p>When our interpereter sees:</p>
<pre><code>const a = {b: "c"}
a.b</code></pre>
<p>Before it runs it, it will transform it into:</p>
<pre><code>const a = makeDict([["b", "c"]])
a("b")</code></pre>
<em>
I'm going to cheat a bit here and allow us to assign to these dicts too, you'll have to imagine how this might be implemented.
</em>
<pre><code>d["qux"] = "4"</pre></code>
<p>
Now for one last bit of built-in language, we're going to introduce a way to get all the keys of an object as an array:
</p>
<pre><code>>>> Object.keys({a: "1", b: "2"})
["a", "b"]</pre></code>
<h2>Implementing classes</h2>
<p>
Now, for the tough bit, we want to make an interface that looks pretty similar to python-style classes:
</p>
<pre id="js-aim"><code>const Animal = ClassMaker({
__init__: (self, name, age) => {
self.name = name
self.age = age
},
greet: (self) => {
return "My name is " + self.name + " and I am " + self.age + " years old"
},
})
>>> const daisy = Animal("Daisy", "32")
>>> daisy.age
"32"
>>> daisy.greet()
"My name is Daisy and I am 32 years old"</pre></code>
<p>
It should be easy to imagine how the interpereter would translate our <a href="#aim">original form</a> into the above, similar to how it translates <code>a = {b: "c"}</code> into <code>a = makeDict([["b", "c"]])</code>.
</p>
<p>
Let's start by demonstrating how we can attach some functions to an instance (in this case called <code>someInstance</code>) using closures:
</p>
<pre><code>// cls is a dict of str -> function
const cls = {
__init__: (self, name, age) => {
self.name = name
self.age = age
},
greet: (self) => {
return "My name is " + self.name + " and I am " + self.age + " years old"
},
}
// someInstance starts as an empty dict
const someInstance = {}
// we populate someInstance with the functions in cls,
// but we use closures to attach someInstance to the
// first argument of each function
for (const k of Object.keys(cls)){
const f = cls[k]
const appliedF = (...args) => {
return f(someInstance, ...args)
}
someInstance[k] = appliedF
}</pre></code>
<p>
Now we can initialise <code>someInstance</code> and call <code>.greet()</code>.
</p>
<pre><code>>>> someInstance.__init__("Rodney", "63")
>>> someInstance.greet()
"My name is Rodney and I am 63 years old"</code></pre>
<p>
Nearly there, now we wrap up the loop and the <code>__init__</code> into <code>ClassMaker</code> such that <code>someInstance</code> (in this case named <code>self</code>) isn't in the global scope:
</p>
<pre><code>const ClassMaker = (cls) => {
// it will returns a function that takes arguments that
// we pass to __init__
return (...initArgs) => {
// create the self and attach the functions as above
const self = {}
for (const k of Object.keys(cls)){
const f = cls[k]
const appliedF = (...args) => {
return f(self, ...args)
}
self[k] = appliedF
}
// call __init__ and then return the instance
self.__init__(...initArgs)
return self
}
}</pre></code>
<p>
That's it, OO enlightenment, closures ≈ objects! Test <a href="#js-aim">Daisy</a> in the console to make sure it works.
</p>
<br>
<br>
<br>
<h2>Bonus section - inheritance</h2>
<p>
Just for good measure, let's add inheritance:
</p>
<pre><code>const ClassMaker = (parent, cls) => {
const combined = {}
for (const k of Object.keys(parent)){
combined[k] = parent[k]
}
for (const k of Object.keys(cls)){
if (cls[k]){
combined[k] = cls[k]
}
}
const init = (...initArgs) => {
const self = {}
for (const k of Object.keys(combined)){
self[k] = (...args) => {
return combined[k](self, ...args)
}
}
self.__init__(...initArgs)
return self
}
for (const k of Object.keys(combined)){
init[k] = combined[k]
}
return init
}</pre></code>
<p>
Now we can do:
</p>
<pre><code>const Animal = ClassMaker(Object(), {
__init__: (self, name, age) => {
self.name = name
self.age = age
},
greet: (self) => {
return "My name is " + self.name + " and I am " + self.age + " years old"
},
})
const Dog = ClassMaker(Animal, {
bark: (self, owner) => {
return self.name + ": Hullo " + owner
}
})</pre></code>
</body>