dynamic-language features
- Sequences and hashmaps have autovivication
- Indirect
require()can change the language - Operators follow Lua semantics
- There are many elements of 'weak' typing
- Lua grants convenient reflection and compile-time codegen
Sequences and hashmaps have autovivication
If looking at just past the last element of a sequence, the sequence is extended.
require 'sequence' local seq: sequence(integer) print(#seq) -- 1 for i=1, 10 do assert(seq[i] == 0) end print(#seq) -- 10
The sequence is extended by the zero of the sequence's subtype, which (per ZII) can be useful even if it's more complex than than an integer:
require 'sequence'
require 'io'
local seq: sequence([2][5]boolean)
seq[1][1][2] = true
seq[1][0][4] = true
seq[1][0][0] = true
for i, a in ipairs(seq) do
for _, y in ipairs(a) do
for _, x in ipairs(y) do
io.write(x and 'X' or '_')
end
io.write('\n')
end
end
io.flush()
-- output:
-- X___X
-- __X__And it can be useful even if it isn't a value type:
require 'sequence'
require 'io'
local maps: sequence(hashmap(integer, hashmap(integer, boolean)))
maps[1][1][2] = true
maps[1][0][4] = true
maps[1][0][0] = true
maps[2][1][3] = true
maps[2][0][3] = true
maps[2][0][0] = true
for i, a in ipairs(maps) do
for x=0, 1 do
for y=0, 4 do
io.write(a[x][y] and 'X' or '_')
end
io.write('\n')
end
io.write('\n')
end
io.flush()
--[[ output:
X___X
__X__
X__X_
___X_
]]Although the previous example, if rewritten to use sequences only, would need a :resize() step, as you can't write to e.g. index 4 of a zero sequence.
Indirect require() can change the language
An easy example is the iterator library, which is required by almost anything else you require, which can create the illusion that iterator functions are core parts of the language. You may only notice that they need a library when you're running little tests like those on this page.
Operators follow Lua semantics
In particular, == can be used between any type, which makes it easy to create accidental truisms. This loop will never find the 'e':
require 'iterators'
require 'string'
local function typeof(v: auto) return #[v.type.name]# end
for _, c in ipairs('hello') do
if c == 'e' then print('found an e') end
assert(typeof(c) == 'uint8')
assert(typeof('e') == 'string')
end
The fix is to check against a byte instead, 'e'_u8.
Generated C:
if(false) {
nelua_print_1(((nlstring){(uint8_t*)"found an e", 10}));
}More C:
int nelua_main(int argc, char** argv) {
if((nelua_sequence_int64____len((&taut_rowdata)), true)) {
nelua_print_1(((nlstring){(uint8_t*)"example tautology", 17}));
}
return 0;
}That's C's comma operator, throwing away the function call's return and replacing it with true. This stuff looks too deliberate to a C compiler for -Wtautological-compare to complain.
Here's another example:
function List:print()
local sb: stringbuilder <close>
local list = self.head
sb:writebyte('{'_u8)
while list ~= nil do
sb:write(list.car)
if list.cdr ~= nil then sb:write(', ') end
list = list.cdr
end
sb:writebyte('}'_u8)
local s <close> = sb:promote()
print(s)
endSee the problem?
If not, does this C (from gdb) help?
543 void list1_List_print(list1_List_ptr self) {
544 nelua_stringbuilderT sb = (nelua_stringbuilderT){0};
545 list1_Cons_ptr list = self->head;
546 nelua_stringbuilderT_writebyte_1((&sb), 123U, NELUA_NIL);
547 while((list, NELUA_NIL, true)) {
548 nelua_stringbuilderT_write_2((&sb), list->car);That's an infinite loop. Because the value I should've written was nilptr, not nil.
There are many elements of 'weak' typing
Suppose you're working without the GC and you have a value of the type sequence(string). To destroy this structure, should you loop over it and destroy each individual string first?
The answer is: there isn't enough information for an answer. string is not like std::string in C++ which always owns its allocation, but easily refers to static memory, or points within some other allocation, and if such strings are mixed into the same structure then it's no longer practical to free it at all. The language provides static strings from literals, and the stdlib provides aliasing strings with e.g. stringbuilderT:view. This isn't a weakness in the type system (hence, the term is a bit misdirecting and pejorative) but it is still a real part of the practice of the language. You can, for example, define a stringview and a allocatedstring and take care to never confuse them with a static string in your own code, permitting more type errors to be caught at compiletime:
local allocatedstring = @record{data: *[0]byte, size: usize}
local stringview = @record{data: *[0]byte, size: usize}
local cstringview = @record{data: cstring, size: usize} -- definitely has a NUL terminator
local taintedstring = @record{data: *[0]byte, size: usize} -- from potentially hazardous I/OBut then you will discover that type-inferred construction, which is a really convenient and and pleasant part of the language, also makes it easy to construct a value without minding the intended restraints of its type:
require 'string'
function allocatedstring:destroy() ((@string){self.data, self.size}):destroy() end
local function consume(st: allocatedstring)
-- do something
st:destroy()
end
consume({"a long string", 6})On the other hand, consume("a long string") no longer works because these new types shed the conversions defined for string. You really can opt-in to stronger typing where you want it. You can also use more explicit constructions such as consume((@taintedstring){"a long string", 6}):
error: in call of function 'consume' at argument 1: no viable type conversion from 'taintedstring' to 'allocatedstring'
consume((@taintedstring){"a long string", 6})
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lua grants convenient reflection and compile-time codegen
Stuff you'd normally only see in a language like Perl, where the programmer for whatever reason decided to synthesize some function names instead of repeat himself. Stuff that Nim or D are also somewhat capable of, but still tend not to do because programmer repetition is too competitive when compared to a once-off macro or template/mixin hack in those languages. For example:
local ids = @record{}
global ids.pluginInstalls: hashmap(string, int)
global ids.themes: hashmap(string, int)
global ids.sites: hashmap(string, int)
global ids.users: hashmap(string, int)
global ids.hosts: hashmap(string, int)
defer
## for name in string.gmatch("pluginInstalls themes sites users hosts", "%S+") do
for k, _ in pairs(ids.#|name|#) do k:destroy() end
ids.#|name|#:destroy()
## end
endThat's not a big deal, right? I have a bunch of similar hashmaps. I want to clean them up. I do that. The compile-time parts are very distinct, and it's easy to understand, and I'm even using Lua string-matching out of pure convenience which is still very easy to understand. This specific example could be replicated by a pure string-processing preprocessor, and doesn't show much Lua knows about the preceding Nelua: Lua's the compiler--it knows everything about the preceding Nelua! But also, I'd hate to have to deal with a string-processing preprocessor. Lua justifies itself while also being convenient in small ways like this.