Lua Notes

nelua generics
Login

generics

  1. auto &co.
  2. maybe(T)
  3. constant(T, k)

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))     --  3

maybe(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 '---'
end

That 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 maybe

And 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;
}