ImmuTable
This is a Lua library that allows the creation of lightweight immutable tables.
Build
This code has been tested with Lua 5.1.5, Lua 5.2.3, Lua 5.3.5, and LuaJIT 2.0.5. To build, simply make or for LuaJIT: make LUA=luajit (you can also optionally specify LUA_INC=<path to the directory containing lua.h and lauxlib.h> or LUA_BIN=<path to the directory containing the lua binary>).
Or, with luarocks: luarocks make immutable-table-scm-1.rockspec
Usage
Here's a simple implementation of a 2-d vector using immutable tables:
immutable = require "immutable"
Vec = immutable({"x","y"}, {
name='Vector',
len2=function(self)
return self.x*self.x + self.y*self.y
end,
class_variable = "classvar",
__add=function(self, other)
local cls = getmetatable(self)
return cls(self.x+other.x, self.y+other.y)
end,
})
v = Vec(2, 3)
assert(v.x == 2 and v.y == 3)
also_v = Vec(2, 3)
assert(v == also_v)
t = {[v]='yep'}
assert(t[also_v] == 'yep')
assert(v + Vec(0,1) == Vec(2,4))
assert(#v == 2)
assert(v:len2() == 13)
assert(v.class_variable == "classvar")
assert(tostring(v) == 'Vector(x=2, y=3)')
assert(Vec:is_instance(v) and not Vec:is_instance({x=2,y=3}))
for k, v in pairs(v) do
assert((k == 'x' and v == 2) or (k == 'y' and v == 3))
end
DifferentVec = immutable({"x","y"})
assert(DifferentVec(1,2) ~= Vec(1,2))
Immutable tables work similarly to regular Lua tables, except that nil can be explicitly stored in an immutable table. The first arguments to the constructor are used for named fields, and all extra arguments are stored in numeric indices.
local Foo = immutable({"x","y"})
local f = Foo(4,5,6,7)
assert(f.x == 4 and f.y == 5 and f[1] == 6 and f[2] == 7)
assert(#f == 2)
for k,v in pairs(f) do
print(k.."="..v) -- prints 1=6,2=7,x=4,y=5
end
for i,v in ipairs(f) do
print(k.."="..v) -- prints 1=6,2=7
end
Singleton recipe
Singletons are pretty straightforward:
Singleton = immutable()
assert(Singleton() == Singleton())
DifferentSingleton = immutable()
assert(Singleton() ~= DifferentSingleton())
Or if you want methods/class variables:
DogSingleton = immutable({}, {name="DogSingleton", bark=function(self) print("woof") end})
DogSingleton():bark()
Tuples
If the number of arguments is greater than the number of field names when constructing an immutable table, the extra values are stored in numeric indices. This can be used to create immutable tables that behave like Python's tuples, by using no field names:
local Tuple = immutable({})
local t0 = Tuple()
local t1 = Tuple(1,2)
local t2 = Tuple(1,2,3,4,5)
assert(#t2 == 5)
assert(t0 == Tuple())
assert(({[t1]='yep'})[Tuple(1,2)])
assert(tostring(Tuple(1,2)) == "(1, 2)")
__new Metamethods
This library adds support for a new user-defined metamethods: __new. __new is called when an instance is created. It takes as arguments the immutable class and all arguments the user passed in, and whatever values it returns are used to create the instance. This is pretty handy for default or derived values:
local Foo = immutable({"x","y","xy"}, {
__new = function(cls, x, y)
y = y or 3
return x, y, x*y
end
})
assert(Foo(2).xy == 6)
The library also defines a __hash metamethod that returns the hash value used internally for instances. Overriding this value does not affect the underlying implementation, but you may find it useful for overriding __index. This is approximately the behavior of the normal __index implementation:
local Foo = immutable({"x","y"}, {
classvar = 23,
__index = function(self, key)
local cls = getmetatable(self)
if cls.__indices[key] ~= nil then
local value = cls.__instances[cls.__hash(self)][self][key]
return value == nil and "nil inst value" or value
else
local value = cls[key]
return value == nil and "undefined value" or value
end
end
})
local f = Foo(1,nil)
assert(f.x == 1 and f.y == "nil inst value" and f.classvar == 23 and f.asdf == 999)
There is also a __super field which points to a table with all of the default implementations of the metamethods, which can be used when overriding:
local
local BiggerOnTheInside = immutable({}, {
__len = function(self, key)
local cls = getmetatable(self)
local len = cls.__super.__len(self)
return math.floor(len/2)
end
})
assert(#BiggerOnTheInside(1,2,3,4,5,6) == 3)
Performance
This library is pretty dang fast, but it's still slower than native Lua tables. Based on my local testing, immutable tables add a couple nanoseconds to table operations in the worst case scenario. In LuaJIT, there is a bigger performance discrepancy because regular tables are more heavily optimized in LuaJIT. Your mileage may vary, but I'd say that immutable tables will probably never be a performance bottleneck for your program, especially if you use them in place of code that already used a constructor function and metatables. In some cases, immutable tables may also help reduce your program's memory footprint (if your program has many duplicate objects in memory) and may even improve speed (e.g. if your program uses a lot of deep equality checks). Don't trust this paragraph though! If in doubt, profile your code!
Implementation details
You can read more about the implementation details in the implementation documentation.
1 # ImmuTable3 This is a Lua library that allows the creation of lightweight immutable tables.5 ## Build7 This code has been tested with Lua 5.1.5, Lua 5.2.3, Lua 5.3.5, and LuaJIT 2.0.5. To build, simply `make` or for LuaJIT: `make LUA=luajit` (you can also optionally specify `LUA_INC=<path to the directory containing lua.h and lauxlib.h>` or `LUA_BIN=<path to the directory containing the lua binary>`).11 ## Usage12 Here's a simple implementation of a 2-d vector using immutable tables:14 immutable = require "immutable"15 Vec = immutable({"x","y"}, {16 name='Vector',17 len2=function(self)18 return self.x*self.x + self.y*self.y19 end,20 class_variable = "classvar",21 __add=function(self, other)22 local cls = getmetatable(self)23 return cls(self.x+other.x, self.y+other.y)24 end,25 })26 v = Vec(2, 3)27 assert(v.x == 2 and v.y == 3)28 also_v = Vec(2, 3)29 assert(v == also_v)30 t = {[v]='yep'}31 assert(t[also_v] == 'yep')32 assert(v + Vec(0,1) == Vec(2,4))33 assert(#v == 2)34 assert(v:len2() == 13)35 assert(v.class_variable == "classvar")36 assert(tostring(v) == 'Vector(x=2, y=3)')37 assert(Vec:is_instance(v) and not Vec:is_instance({x=2,y=3}))38 for k, v in pairs(v) do39 assert((k == 'x' and v == 2) or (k == 'y' and v == 3))40 end41 DifferentVec = immutable({"x","y"})42 assert(DifferentVec(1,2) ~= Vec(1,2))43 ```45 Immutable tables work similarly to regular Lua tables, except that `nil` can be explicitly stored in an immutable table. The first arguments to the constructor are used for named fields, and all extra arguments are stored in numeric indices.47 local Foo = immutable({"x","y"})48 local f = Foo(4,5,6,7)49 assert(f.x == 4 and f.y == 5 and f[1] == 6 and f[2] == 7)50 assert(#f == 2)51 for k,v in pairs(f) do52 print(k.."="..v) -- prints 1=6,2=7,x=4,y=553 end54 for i,v in ipairs(f) do55 print(k.."="..v) -- prints 1=6,2=756 end57 ```59 ## Singleton recipe60 Singletons are pretty straightforward:62 Singleton = immutable()63 assert(Singleton() == Singleton())64 DifferentSingleton = immutable()65 assert(Singleton() ~= DifferentSingleton())66 ```67 Or if you want methods/class variables:69 DogSingleton = immutable({}, {name="DogSingleton", bark=function(self) print("woof") end})70 DogSingleton():bark()71 ```73 ## Tuples74 If the number of arguments is greater than the number of field names when constructing an immutable table, the extra values are stored in numeric indices. This can be used to create immutable tables that behave like Python's tuples, by using no field names:76 local Tuple = immutable({})77 local t0 = Tuple()78 local t1 = Tuple(1,2)79 local t2 = Tuple(1,2,3,4,5)80 assert(#t2 == 5)81 assert(t0 == Tuple())82 assert(({[t1]='yep'})[Tuple(1,2)])83 assert(tostring(Tuple(1,2)) == "(1, 2)")84 ```87 This library adds support for a new user-defined metamethods: `__new`. `__new` is called when an instance is created. It takes as arguments the immutable class and all arguments the user passed in, and whatever values it returns are used to create the instance. This is pretty handy for default or derived values:89 local Foo = immutable({"x","y","xy"}, {90 __new = function(cls, x, y)91 y = y or 392 return x, y, x*y93 end94 })95 assert(Foo(2).xy == 6)96 ```98 The library also defines a `__hash` metamethod that returns the hash value used internally for instances. Overriding this value does not affect the underlying implementation, but you may find it useful for overriding `__index`. This is approximately the behavior of the normal `__index` implementation:100 local Foo = immutable({"x","y"}, {101 classvar = 23,102 __index = function(self, key)103 local cls = getmetatable(self)104 if cls.__indices[key] ~= nil then105 local value = cls.__instances[cls.__hash(self)][self][key]106 return value == nil and "nil inst value" or value107 else108 local value = cls[key]109 return value == nil and "undefined value" or value110 end111 end112 })113 local f = Foo(1,nil)114 assert(f.x == 1 and f.y == "nil inst value" and f.classvar == 23 and f.asdf == 999)115 ```117 There is also a `__super` field which points to a table with all of the default implementations of the metamethods, which can be used when overriding:119 local120 local BiggerOnTheInside = immutable({}, {121 __len = function(self, key)122 local cls = getmetatable(self)123 local len = cls.__super.__len(self)124 return math.floor(len/2)125 end126 })127 assert(#BiggerOnTheInside(1,2,3,4,5,6) == 3)128 ```130 ## Performance131 This library is pretty dang fast, but it's still slower than native Lua tables. Based on my local testing, immutable tables add a couple nanoseconds to table operations in the worst case scenario. In LuaJIT, there is a bigger performance discrepancy because regular tables are more heavily optimized in LuaJIT. Your mileage may vary, but I'd say that immutable tables will probably never be a performance bottleneck for your program, especially if you use them in place of code that already used a constructor function and metatables. In some cases, immutable tables may also help reduce your program's memory footprint (if your program has many duplicate objects in memory) and may even improve speed (e.g. if your program uses a lot of deep equality checks). Don't trust this paragraph though! If in doubt, profile your code!133 ## Implementation details135 You can read more about the implementation details in the [implementation documentation](./implementation.md).