code / lua-immutable

Lines1.0K C473 Lua341 Markdown173 make28
(135 lines)

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 # ImmuTable
3 This is a Lua library that allows the creation of lightweight immutable tables.
5 ## Build
7 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>`).
9 Or, with [luarocks](https://luarocks.org/): `luarocks make immutable-table-scm-1.rockspec`
11 ## Usage
12 Here's a simple implementation of a 2-d vector using immutable tables:
13 ```lua
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.y
19 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) do
39 assert((k == 'x' and v == 2) or (k == 'y' and v == 3))
40 end
41 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.
46 ```lua
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) do
52 print(k.."="..v) -- prints 1=6,2=7,x=4,y=5
53 end
54 for i,v in ipairs(f) do
55 print(k.."="..v) -- prints 1=6,2=7
56 end
57 ```
59 ## Singleton recipe
60 Singletons are pretty straightforward:
61 ```lua
62 Singleton = immutable()
63 assert(Singleton() == Singleton())
64 DifferentSingleton = immutable()
65 assert(Singleton() ~= DifferentSingleton())
66 ```
67 Or if you want methods/class variables:
68 ```lua
69 DogSingleton = immutable({}, {name="DogSingleton", bark=function(self) print("woof") end})
70 DogSingleton():bark()
71 ```
73 ## Tuples
74 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:
75 ```lua
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 ```
86 ## `__new` Metamethods
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:
88 ```lua
89 local Foo = immutable({"x","y","xy"}, {
90 __new = function(cls, x, y)
91 y = y or 3
92 return x, y, x*y
93 end
94 })
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:
99 ```lua
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 then
105 local value = cls.__instances[cls.__hash(self)][self][key]
106 return value == nil and "nil inst value" or value
107 else
108 local value = cls[key]
109 return value == nil and "undefined value" or value
110 end
111 end
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:
118 ```lua
119 local
120 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 end
127 assert(#BiggerOnTheInside(1,2,3,4,5,6) == 3)
128 ```
130 ## Performance
131 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 details
135 You can read more about the implementation details in the [implementation documentation](./implementation.md).