A Lua type library for the weak.
Consider our friendly type function:
type("Hello, World?"): string
We normally check for type equality with this:
type('hello') == 'string' (true)
What if we could also do this:
type('hello', 'string') (string, string)
Not a big deal, but this could lead to some nice tricks:
local My_Object_Type_t = type.new{"My_Object_Type", "string"}
local my_object = type.cast({}, My_Object_Type_t)
or...
local My_Object_Type_t = type.new{"My_Object_Type", "string"}
--some code
---later in your object constructor...
local my_object = {}
My_Object_Type_t(my_object)
or...
local my_object = type.cast({}, {"My_Object_Type", "string"})
type(my_object, 'string') (My_Object_Type, string)
In the last example, My_Object_Type
is the type of the object,
so it is returned first. 'string' was the match, so it is second.
type
called with multiple arguments redirects to type.check
. It receives an object
to check as the first argument, followed by possible matches, which may be either strings
or type
objects. It returns the type object first and the match second.
Note: I think of secondary 'types' more as 'supported interfaces'. They are the quacks in duck typing.
So we see that objects can have multiple entries.
Next, we will make some objects:
--test objects
local foo_t = type.new{"foo", "test"}
local test_t = type.new{"test", "foo", "bar"}
local obj = foo_t({})
local obj1 = test_t({})
Comparison works on types. We can see if all of the interfaces are 'in' another type, if they have all the same type interfaces and the opposites of all of that. Order does not matter in comparison.
In the next example, you will notice that we use 'type.get'. Since we are keeping compatibility the Lua 'type' function, we need to "get" the actual type object, not just its string name.
I may change this later...
Anyway...
Containment:
type.lt(obj, obj1): true --(obj is a subset of obj1)
type.lt(obj1, obj): false
type.get(obj) <= type.get(obj1): true
type.get(obj) >= type.get(obj1): false
type.get(obj1) <= type.get(obj): false
type.get(obj1) >= type.get(obj): true
type.get(obj) == type.get(obj1): false
type.get(obj) < type.get(obj1): true
type.get(obj) > type.get(obj1): false
type.get(obj1) < type.get(obj): false
type.get(obj1) > type.get(obj): true
Equality of two tables seems a bit worthless, except that order is not considered...
Imagine a possible case where it is not. Consider:
local fraction_t = type.new{"fraction", "number", "int"}
local number_t = type.new{"number", "fraction", "int"}
Where a 'fraction' object is a table with a numerator and a denominator and all of its arithmatic and equality operators are defined. It supports the 'number' and 'int' interface and the 'number' type supports fractions and ints.
There is nothing stopping someone from using this in terms of sub-types, but that makes no sense to me. :)
What happens if we try equality? Containment with another couple of objects:
type.eq(number_t, fraction_t): true
number_t == fraction_t: true
type.lt(number_t, fraction_t): false
type.eq(3, fraction_t): true
WARNING!! this last test is tricky. We use the fact that weak_type will promote an object (3) to a type, if it is not a type object. DO NOT think that it will turn...
`"nil"`
...into the nil
type object. You will get string type object, instead!
Even unrelated things work as they should!
local pop_t = type.new{"pop", "bang", "dazzle"}
type.lt(pop_t, test_t): false
type.lt(test_t, pop_t): false
test_t <= pop_t: false
test_t >= pop_t: false
pop_t <= test_t: false
pop_t >= test_t: false
test_t == pop_t: false
test_t < pop_t: false
test_t > pop_t: false
pop_t < test_t: false
pop_t > test_t: false
If you have the debug library loaded, you may also be able to get type names for user data types:
type(io.stdin): userdata
type(io.stdin, "userdata"): FILE*, userdata
You may also pre-cast a type to a user data, making sure to set userdata = true
:
local int64_t = type.new{"int64", "number", userdata = true, table = false}
In this example, we have let Lua know that our type can stand in for a number. With the int64 library written by Luiz Henrique de Figueiredo, you can now use these types more naturally, (at least within your own library funcitons. The lua libraries are not cool with your userdata when they want to see a number. I monkey patch all of my stuff.)
You have int64, so allow me to demonstration:
local int64 = pcall(require, 'int64')
local int3 = int64.new(3)
type(int3): userdata
type(int3, "number"): int64, number
type.lt(3, int3): false
...Wait.. why?
Because we redefined the number
type to include fractions. If we put it back
to the default:
local number_t = type.new{"number", table = false}
then...
type.lt(3, int3): true
type.lt(int3, 3): false
type.lt(type.find"userdata", int3): true
Use lt to see if an object can 'behave' like something, even with native types.
local obj4 = {}
int64_t(obj4)
type.get(3) <= type.get(obj4): true
type.get(3) >= type.get(obj4): false
type.lt(3, obj4): true
type.lt(obj4, 3): false
type(obj4, "number"): int64, number
Again, all of this works (mostly) like normal, too:
type("Hello, word!"): string
type(2): number
type(nil): nil
The 'mostly' is that regular type will error on a call like this:
type()
or this...
local f = function()
return
end
type(f()) --> ESPLODE... as Strong Bad says.
It might not seem completely obvious why this ever comes up,
but there are times when does, especially when walking through
an argument list with select
.
So, instead, this is how it works:
type(f()): undefined, nil
type(f(), "nil"): nil, nil
type(f(), "undefined"): false
type.eq(f(), type.find"nil"): true
type.lt(f(), type.find"nil"): false
type.lt(f(), type.find"undefined"): true
The reason for the above behavior is discovered in the way that Lua adds the "nil" to argument positions, if there are that follow. Therefore, you can only get the 'undefined' type by calling 'type()' without other arguments. If you do not every care if it is 'undefined' or just plain 'nil', like you have for your whole Lua life, you can safely ignore this.
Also, note that no matter what you do (even to __index
), a table will
always return nil
, even for undefined indexes. :(
To sumarize:
type.cast
puts a new type into a table. type.new
returns a new type. A type will cast
an object (table
) by calling it like typ_name_t(my_object)
.
Types are saved in a type.types
table. They cache values (especially usefull for userdata)
and make equality simpler. I have emerging evil plans for more on this, as well. Bwahahaha.
type.get
returns a type
object. type.tostring
returns the string
representation of the type, which is often not the Lua type. Use type(o)
to return's o's
native type (table
or userdata
).
Note that type(obj, 'table')
returns a truthy value (the type
object), as well (foo, table
in
this case), if there is a match. Otherwise, you get false
.
If it isn't abundantly clear, this is a first.5 whack at something useful. Hopefully it's not too un-Lua-like. I tried to stick to real-world problems that I've actually run into. So far, it's worked really well for me.
Your comments would be most appreciated!
-- Andrew Starks