-
Notifications
You must be signed in to change notification settings - Fork 0
/
proto.lua
452 lines (368 loc) · 13.8 KB
/
proto.lua
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
--[[
Author: Darren Schnare
Keywords: lua,prototype,inheritence,constructor,copy,javascript,ecmascript
License: MIT ( http://www.opensource.org/licenses/mit-license.php )
Repo: https://github.com/dschnare/Proto-Lua
]]--
-- Determines if a table instance has a constructor's protoype in its prototype chain.
-- The constructor is expected to be created from 'constructor.create()'. Safe to call
-- with any arguments.
--[[ Example:
local cat = Cat('felix', 'black')
print(cat:instanceof(Cat)) -- true
--]]
local instanceof = function(a, constructor)
local _type, _rawequal = type, rawequal
if (_type(a) ~= 'table') then return false end
if (_type(constructor) ~= 'table' or _type(constructor.prototype) ~= 'table') then return false end
local o = constructor.prototype
while true do
a = a.__proto
if (_type(a) ~= 'table') then return false end
if (_rawequal(o, a)) then return true end
end
end -- instanceof()
return {
-- Determines if a value is a string.
isString = function(v) return type(v) == 'string' end,
-- Determines if a value is a table.
isTable = function(v) return type(v) == 'table' end,
-- Determines if a value is a function.
isFunction = function(v) return type(v) == 'function' end,
-- Determines if a value is defined (not nil).
isDefined = function(v) return type(v) ~= 'nil' end,
-- Determines if a value is nil.
isNil = function(v) return type(v) == 'nil' end,
-- Determines if a value is a number.
isNumber = function(v) return type(v) == 'number' end,
-- Determines if a value is a boolean.
isBoolean = function(v) return type(v) == 'boolean' end,
-- Determines if a value is a thread.
isThread = function(v) return type(v) == 'thread' end,
-- Determines if a value is a userdata.
isUserdata = function(v) return type(v) == 'userdata' end,
--[[
Determines if a table adheres to another table's interface.
This function will iterate over all table key:value pairs in 'b' and compare the built-in type
of a coorisponding key in 'a' if it exists. If the built-in types don't match then return false
otherwise returns true.
In place of 'b' a table with key:value pairs whose values are strings equivalent to the expected built-in type
can be used. The '*' string is a special type that will expect the key to at least exist on 'a', but can be of
any built-in type.
]]--
adheresTo = function(a, b)
local _type = type
if (_type(a) == 'table' and _type(b) == 'table') then
for k,v in pairs(b) do
if (v == '*' and _type(a[k]) ~= 'nil') then return true end
if (_type(v) == _type(a[k]) or _type(v) == a[k]) then return true end
end
return false
else
return _type(a) == _type(b)
end
end, -- adheresTo()
--[[
Copies all key:value pairs of each table passed to the function into the first table of the argument list.
If the first argument is not a valid table then one will be created. Existing key:value pairs on the table
will be overwritten.
Returns the first table or the newly created table.
mixin()
mixin(t)
mixin(t, t1, t2, ..., tn)
]]--
mixin = function(...)
local args, _type, a = {...}, type, nil
local t, _pairs = args[1], pairs
if (_type(t) ~= 'table') then t = {} end
for i=1,#args do
if (i ~= 1) then
a = args[i]
if (_type(a) == 'table') then
for k,v in _pairs(a) do t[k] = v end
end
end
end
return t
end, -- mixin()
-- Determines if a table instance has a constructor's protoype in its prototype chain.
-- The constructor is expected to be created from 'constructor.create()'. Safe to call
-- with any arguments.
instanceof = instanceof,
-- Promotes all members of an instance's prototype chain to be instance members. Since all members will be at the
-- instance level their access is much faster than if they were left deep in the prototype chain.
-- A drawback of this is that the instance is now overriding its entire prototype chain which
-- means any changes to the chain will not be reflected in the instance. However, new members added
-- to the prototype chain will be available immediately as usual. Setting an instance member to 'nil'
-- will restore typical prototypal behaviour. Returns the table passed in.
promote = function(t)
local _type, _rawget, _rawset, _pairs = type, rawget, rawset, pairs
if (_type(t) == 'table') then
local p = t.__proto
while (_type(p) == 'table') do
for k,v in _pairs(p) do
if (_rawget(t, k) == nil) then
_rawset(t, k, v)
end
end
p = p.__proto
end
end
return t
end, -- promote()
-- Demotes all instance level members that exist on the instance's prototype chain. This function
-- will perform the exact opposite of promote() (i.e. set all instance members to nil that exist
-- on the prototype chain). Returns the table instance passed in.
demote = function(t)
local _type, _rawget, _rawset, _pairs, p = type, rawget, rawset, pairs, nil
if (_type(t) == 'table') then
p = t.__proto
if (_type(p) == 'table') then
for k,v in _pairs(t) do
if (p[k] == _rawget(t, k)) then
_rawset(t, k, nil)
end
end
end
end
return t
end, -- demote()
--[[
The constructor namespace.
]]--
constructor = {
--[[
Creates a new constructor with the specified members and base prototype. If 'base' is a constructor then the
constructor's prototype will be used. All key:value pairs in the 'members' table will be added to the
constructor's prototype, so the keys on 'members' are prototype properties.
All constructors have the following properties:
- prototype The constructor's prototype table. Any key:value pairs on this table will be available on all
instances. Any new key:value pairs added after instances have been created will be available
immediately on all instances.
When a constructor is called as a function it will return a new table instance that inherits all properties
from the constructor's prototype and its base prototype and so on. If an 'init' method exists on the instance
either from its members or protytpe chain then it will be called with the arguments passed to the constructor.
If a constructor is called with a single table argument that was created by the constructor being called and
the instance being created has a 'copy' method, then the 'copy' method will be called with the other table
instance to be copied from. This pattern is the copy-constructor pattern similar to C++.
All instances created from a constructor have at least the following properties:
- constructor A reference back to the constructor that created the instance.
- instanceof A method that tests the prototype chain of the instance. When called with a constructor will
return true if the instance has the constructor's prototype in its prototype chain,
false otherwise.
- __proto A reference to the instance's prototype. This can be modified at runtime.
create()
create(members)
create(base, members)
]]--
create = (function()
-- Locals for the 'create' function.
-- The default base prototype for all constructors that don't have a base prototype.
local defaultBase = {}
-- The metatable used for ALL instances created with constructors created from this API.
-- This saves substantially on memory usage compared to creating a new metatable for each constructor.
local metatable = {
__add = function(self, other)
local __add = self.__add
if (type(__add) == 'function') then
return __add(self, other)
else
error('Table does not have an "add" metamethod.')
end
end,
__sub = function(self, other)
local __sub = self.__sub
if (type(__sub) == 'function') then
return __sub(self, other)
else
error('Table does not have a "sub" metamethod.')
end
end,
__mul = function(self, other)
local __mul = self.__mul
if (type(__mul) == 'function') then
return __mul(self, other)
else
error('Table does not have a "mul" metamethod.')
end
end,
__div = function(self, other)
local __div = self.__div
if (type(__div) == 'function') then
return __div(self, other)
else
error('Table does not have a "div" metamethod.')
end
end,
__mod = function(self, other)
local __mod = self.__mod
if (type(__mod) == 'function') then
return __mod(self, other)
else
error('Table does not have a "mod" metamethod.')
end
end,
__pow = function(self, other)
local __pow = self.__pow
if (type(__pow) == 'function') then
return __pow(self, other)
else
error('Table does not have a "pow" metamethod.')
end
end,
__unm = function(self)
local __unm = self.__unm
if (type(__unm) == 'function') then
return __unm(self)
else
error('Table does not have an "unm" metamethod.')
end
end,
__concat = function(self, other)
local __concat = self.__concat
if (type(__concat) == 'function') then
return __concat(self, other)
else
error('Table does not have a "concat" metamethod.')
end
end,
__eq = function(self, other)
local __eq = self.__eq
if (type(__eq) == 'function') then
return __eq(self, other)
else
if type(self) ~= type(other) then -- different types?
return false -- different objects
end
return rawequal(self, other)
end
end,
__lt = function(self, other)
local __lt = self.__lt
if (type(__lt) == 'function') then
return __lt(self, other)
else
error('Table does not have a "lt" metamethod.')
end
end,
__le = function(self, other)
local __le = self.__le
if (type(__le) == 'function') then
return __le(self, other)
else
error('Table does not have a "le" metamethod.')
end
end,
__index = function(self, key)
local value, __proto = nil, self.__proto
if (type(__proto) == 'table') then
value = __proto[key]
end
if (value ~= nil) then return value end
local __index = rawget(self, '__index')
if (__index ~= true and type(__proto) == 'table') then
__index = __proto.__index
end
if (__index) then
if (type(__index) == 'function') then
return __index(self, key)
elseif (type(__index) == 'table') then
return __index[key]
end
end
return value
end,
__newindex = function(self, key, value)
local __proto = self.__proto
local __newindex = self.__newindex
if (type(__newindex) == 'function') then
__newindex(self, key, value)
else
rawset(self, key, value)
end
end,
__call = function(self, ...)
local __call = self.__call
if (type(__call) == 'function') then
return __call(self, ...)
else
error('Table does not have a "call" metamethod.')
end
end,
__tostring = nil
}
function metatable:__tostring()
local __tostring = self.__tostring
if (type(__tostring) == 'function') then
return __tostring(self)
else
__tostring = metatable.__tostring
metatable.__tostring = nil
local str = tostring(self)
metatable.__tostring = __tostring
return str
end
end
-- Initializes a table with a readonly '__proto' key referencing base and ensures
-- prototype is a valid table.
local initPrototype = function(prototype, base)
if (type(prototype) ~= 'table') then prototype = {} end
if (type(base.prototype) == 'table') then base = base.prototype end
-- All prototypes have an 'instanceof' method for convenience.
prototype.instanceof = instanceof
return setmetatable(prototype, {
__index = function(self, key)
if (key == '__proto') then return base end
return base[key]
end,
__newindex = function(self, key, value)
if (key ~= '__proto') then
rawset(self, key, value)
end
end
})
end -- initPrototype()
-- The 'create' function.
return function(base, members)
local prototype = nil
if (type(members) ~= 'table') then
members = base
base = nil
end
if (type(base) ~= 'table') then base = defaultBase end
-- Initialize the members as our constructor's prototype.
prototype = initPrototype(members, base)
-- Nil out the members and base table
members = nil
base = nil
-- This is our constructor we are creating. Make sure we give a property that points to our prototype.
local constructor = {prototype = prototype}
-- Set the metatable for our constructor, making it callable.
setmetatable(constructor, {
__call = function(self, ...)
local instance = setmetatable({constructor = constructor, __proto = prototype}, metatable)
local other = ...
-- If a single table is passed in as an argument and its constructor equals our constructor
-- and we have a '__copy' method (i.e. copy operation) then this call to our constructor is
-- really a 'copy constructor' call. When this occurs we do not call the '__init' method before returning.
if (select('#', ...) == 1
and type(other) == 'table'
and other.constructor == constructor
and other.__proto == constructor.prototype
and type(instance.__copy) == 'function') then
instance:__copy(other)
return instance
end
-- If we have an '__init' method then we call it with all of the arguments passed to our constructor.
if (type(instance.__init) == 'function') then
instance:__init(...)
end
-- Return the newly created instance.
return instance
end
})
return constructor
end -- create()
end)() -- create() closure
} -- constructor {}
} -- proto namespace {}