generics
auto &co.
The simplest way to write generic code is to write a function that takes an auto parameter, or varargs, or a concept. You can still bring in the preprocessor to find out what precise type(s) were received, as this function is specialized to the types it's given.
local function add(a: auto, b: auto)
## if a.type.is_stringy and b.type.is_stringy then
return a .. b
## else
return a + b
## end
end
require 'string'
print(add('a', 'b')) -- ab
print(add(1, 2)) -- 3maybe(T)
The next simplest example of generic code is a discriminated union with a boolean as the discriminant and, er, no union.
## local function make_maybeT(T)
local maybeT = @record{
obj: #[T]#,
ok: boolean,
}
## return maybeT
## end
local maybe = #[generalize(make_maybeT)]#
return maybe
make_maybeT is a Lua function that returns a Nelua type, a record whose .obj field type is taken from the parameter to make_maybeT. maybe then is a Nelua type which behaves like a compile-time function that returns a type, with the Lua function generalize() making that work.
Usage:
require 'iterators'
require 'string'
local optional = require 'maybe'
local Row = @record{
id: integer,
name: optional(string),
age: optional(integer),
tab: optional(integer), -- cents
}
local function just(x: auto)
local T = #[x.type]#
return (@optional(T)){x, true}
end
local rows: []Row = {
{1, {"owner", true}, {30, true}, {}},
{2, just "steve", just(20), just(500)},
}
for _, row in ipairs(rows) do
print('name:', row.name.ok and row.name.obj or '(unknown)')
print('age :', row.age.ok and tostring(row.age.obj) or '(unknown)')
if row.tab.ok and row.tab.obj > 0 then
print('OWES US MONEY')
end
print '---'
endThat all works, but this other use exposes some inconveniences:
local maybe = require 'maybe'
local function is_none(n: maybe(integer))
return not n.ok
end
print(is_none({}))
print(is_none({1, true}))
print(is_none(3))First, is_none() should really be defined against any kind of maybe(T), and not just maybe(integer). Second, the last line's error message refers to a maybeT instead of maybe(integer):
error: in call of function 'is_none' at argument 1: no viable type conversion from 'int64' to 'maybeT'
print(is_none(3))
^~~
To improve on these inconveniences, here's another try:
## local function make_maybeT(T)
## static_assert(traits.is_type(T), "invalid type '%s'", T)
local T = #[T]#
local maybeT: type <nickname(#[string.format('maybe(%s)', T)]#)> = @record{
obj: T,
ok: boolean,
}
## maybeT.value.subtype = T
## maybeT.value.is_maybe = true
## return maybeT
## end
local maybe: type = #[generalize(make_maybeT)]#
global maybe.concept = #[concept(function(attr) return attr.type.is_maybe end)]#
return maybeAnd usage:
local maybe = require 'maybe2'
local function is_none(n: maybe.concept)
return not n.ok
end
print(is_none((@maybe(integer))({})))
--print(is_none({1, true})) -- type 'table' can't match concept
--print(is_none(3)) -- type 'int64' could not match concept 'maybe.concept'
-- just to show off the nickname
local function is_none(n: maybe(integer))
return not n.ok
end
print(is_none(3)) -- no viable type conversion from 'int64' to 'maybe(int64)'This still has an inconvenience, that type inference doesn't work as well as with a concrete type like `maybe(integer)`, but it solves the earlier problems.
constant(T, k)
How about parameterizing by a value rather than a type? For example, an array might have a fixed size, or a CSV reader might fix the separator at compile-time. That's straightforward:
## local function make_constant(T, k)
local r = @record{}
function r.constant(): #[T]#
return #[k]#
end
function r:constantmethod(): #[T]#
return #[k]#
end
## return r
## end
local constant = #[generalize(make_constant)]#
local f = @constant(integer, 10)
local g: constant(integer, 5)
print(f.constant(), g:constantmethod())
-- print(g.constant()) -- error: cannot index field 'constant' on value of type 'r'
print(#[g.type.metafields.constant]#())But there are some subtle aspects to it. Can you tell why f.constant() works but g.constant() doesn't?
The answer: f is a type and g is a record. You could go on to write
local h: f print(h:constantmethod()) -- 10 print(#[g.type]#.constant()) -- 5
This all results in the following C functions:
int64_t constant_r_constant(void) {
return 10;
}
int64_t constant_r_1_constantmethod(constant_r_1_ptr self) {
return 5;
}