Repository: stevedonovan/Microlight
Branch: master
Commit: e08ccaa32b79
Files: 13
Total size: 65.3 KB
Directory structure:
gitextract_l7w06r43/
├── compress.lua
├── config.ld
├── examples/
│ └── test.lua
├── microlight-1.1-1.rockspec
├── ml.lua
├── ml_module.lua
├── ml_properties.lua
├── ml_range.lua
├── mod52.lua
├── readme.md
├── test
├── test-mod52.lua
└── test.bat
================================================
FILE CONTENTS
================================================
================================================
FILE: compress.lua
================================================
io.output 'compressed/ml.lua'
for line in io.lines 'ml.lua' do
if not (line:match '^%s*$' or line:match '^%-%-') then
line = line:gsub('^%s*','')
io.write(line,'\n')
end
end
io.close()
================================================
FILE: config.ld
================================================
project='Microlight'
file='ml.lua'
description='Compact Lua Utility Library'
format='markdown'
title='Microlight'
readme='readme.md'
================================================
FILE: examples/test.lua
================================================
local ml = require 'ml'
local A = ml.Array
unpack = unpack or table.unpack
ml.import(_G,ml)
local teq = Array.__eq
function asserteq (v1,v2)
local t1,t2 = type(v1),type(v2)
local check = false
if t1 == t2 then
if t1 ~= 'table' then check = v1 == v2
else check = teq(t1,t2)
end
end
if not check then
error("assertion failed\nA "..tstring(v1).."\nB "..tstring(v2),2)
end
end
t = {one={two=2},10,20,{1,2}}
assert(tstring(t) == "{10,20,{1,2},one={two=2}}")
local charmap = string.char(unpack(range(0,255)))
assert(("aa"..charmap.."bb"):match(escape(charmap)) == charmap)
assert(split('hello','')[1] == 'hello')
local a123 = {'one','two','three'}
asserteq(split('one,two,three',','),a123)
asserteq(split('one,,two',','),{'one','','two'})
-- trailing delimiter ignored!
asserteq(split('one,two,three,',','),a123)
-- delimiter is a Lua pattern (use escape if necessary!)
-- splitting tokens separated by commas and/or spaces
asserteq(split('one, two,three ','[,%s]+'),a123)
asserteq(split('one two three ','[,%s]+'),a123)
--- paths
-- note that forward slashes also work on Windows
local P = '/users/steve/bonzo.dog'
local path,name = ml.splitpath(P)
asserteq(path,'/users/steve')
asserteq(name,'bonzo.dog')
local basename,ext = ml.splitext(P)
asserteq(basename,'/users/steve/bonzo')
asserteq(ext,'.dog')
t = {10,20,30,40}
asserteq(sub(t,1,2),{10,20})
asserteq(indexof(t,20),2)
-- indexof may have an optional specialized equality function
idx = indexof({'one','two','three'},'TWO',function(s1,s2)
return s1:upper()==s2:upper()
end
)
asserteq (idx,2)
-- generalization of table indexing
asserteq(indexby(t,{1,4}),{10,40})
-- generating a range of numbers (Array.range does this but returns an Array)
asserteq(range(1,4),{1,2,3,4})
-- append extra elements to t
extend(t,{50,60},{70,80})
asserteq(sub(t,4),{40,50,60,70,80})
m = makemap(t)
assert(m[50]==5 and m[20]==2)
assert(count(m) == #t)
tt = keys(m)
-- this isn't necessarily true because there is no guaranteed key order
--assert(tt==A(t))
-- this only compares keys, so the actual values don't matter
assert(issubset({a=1,b=2,c=3},{a='hello'}))
removerange(t,2,3)
asserteq(t,{10,40,50,60})
-- insert some values at the start
-- (insertvalues in general can be expensive when actually inserting)
insertvalues(t,1,{2,5})
asserteq(t,{2,5,10,40,50,60})
-- copy some values without inserting (overwrite)
insertvalues(t,2,{11,12,13},true)
asserteq(t,{2,11,12,13,50,60})
-- make a new array containing all the even numbers
-- the filter method is equivalent to ml.ifilter
ta = A(t)
a2 = ta:filter(function(x) return x % 2 == 0 end)
asserteq(a2,A{2,12,50,60})
ta = A{10,2,5,4,9}
ta:sort()
asserteq(ta,A{2,4,5,9,10})
-- make a new array by mapping the square function over its elements
-- the map method is equivalent to ml.imap
a3 = Array.range(1,4):map(function(x) return x*x end)
asserteq(a3,A{1,4,9,16})
-- the result of a map must have the same size as the input,
-- and must be a valid sequence, so `nil` becomes `false`
t = {'1','foo','10'}
res = imap(tonumber,t)
asserteq(res,{1,false,10})
-- Array objects understand concatenation!
asserteq(A{1,2}..A{3,4},Array.range(1,4))
a = Array.range(1,10)
assert(a:sub(2,4) == A{2,3,4})
t = {one=1,two=2}
k = keys(t)
-- no guarantee of order!
assert(teq(k,{'one','two'}) or teq(k,{'two','one'}))
-- ml does not give us a corresponding values() function, but
-- collect2nd does the job
v = collect2nd(pairs(t))
assert(teq(v,{1,2}) or teq(v,{2,1}))
----- functional helpers
assert( bind1(string.match,'hello')('^hell') == 'hell')
isdigits = bind2(string.match,'^%d+$')
assert( isdigits '23105')
assert( not isdigits '23x5' )
local k = 0
f = memoize(function(s)
k = k + 1
return s:upper()
end)
assert(f'one' == 'ONE')
assert(f'one' == 'ONE')
assert(k == 1)
-- string lambdas ---
-- Contain up to three placeholders: X,Y and Z
local a = A{1,2,3,4}
local plus1 = a:map 'X+1'
assert (plus1 == A{2,3,4,5})
-- can use extra placeholder to match extra arg...
assert(a:map('X+Y',1), plus1)
val = A{'ml','test','util'}:map('X..Y','.lua'):filter(exists)
assert(val == A{'ml.lua'})
--- classes ------
C = class()
--- conventional name for constructor --
function C:_init (name)
self.name = name
end
-- can define metamethods as well as plain methods
function C:__tostring ()
return 'name '..self.name
end
function C:__eq (other)
return self.name == other.name
end
c = C('Jones')
assert(tostring(c) == 'name Jones')
-- inherited classes inherit constructors and metamethods
D = class(C)
d = C('Jane')
assert(tostring(d) == 'name Jane')
assert(d == C 'Jane')
-- if you do have a constructor, call the base constructor explicitly
E = class(D)
function E:_init (name,nick)
self:super(name)
self.nick = nick
end
-- call methods of base class explicitly
-- (you can also use `self._class`)
function E:__tostring ()
return D.__tostring(self)..' nick '..self.nick
end
asserteq(tostring(E('Jones','jj')),'name Jones nick jj')
--- Subclassing Array
Strings = class(Array)
-- can always use the functional helpers to make new methods
-- bind2 is useful with methods
Strings.match = bind2(Strings.filter,string.match)
a = Strings{'one','two','three'}
asserteq(a:match 'e$',{'one','three'})
--- for numerical operations
NA = class(Array)
local function mapm(a1,op,a2)
local M = type(a2)=='table' and Array.map2 or Array.map
return M(a1,op,a2)
end
--- elementwise arithmetric operations
function NA.__unm(a) return a:map '-X' end
function NA.__pow(a,s) return a:map 'X^Y' end
function NA.__add(a1,a2) return mapm(a1,'X+Y',a2) end
function NA.__sub(a1,a2) return mapm(a1,'X-Y',a2) end
function NA.__div(a1,a2) return mapm(a1,'X/Y',a2) end
function NA.__mul(a1,a2) return mapm(a2,'X*Y',a1) end
function NA:minmax ()
local min,max = math.huge,-math.huge
for i = 1,#self do
local val = self[i]
if val > max then max = val end
if val < min then min = val end
end
return min,max
end
function NA:sum ()
local res = 0
for i = 1,#self do
res = res + self[i]
end
return res
end
function NA:normalize ()
return self:transform('X/Y',self:sum())
end
NA:mappers {
tostring = tostring,
format = string.format
}
asserteq(NA{10,20}:tostring(),{'10','20'})
asserteq(NA{1,2.2,10}:format '%5.1f',{" 1.0"," 2.2"," 10.0"})
--- arithmetric --
asserteq(NA{1,2,3} + NA{10,20,30}, NA{11,21,31})
-- note 2nd arg may be a scalar
asserteq(NA{1,2,3}+1, NA{2,3,4})
asserteq(NA{10,20}/2, NA{5,10})
-- except for * where the 1st arg can be a scalar...
asserteq(2*NA{1,2,3},NA{2,4,6})
asserteq(-NA{1,2},NA{-1,-2})
-- subclasses of Array have covariant methods, so that e.g. sub
-- is returning an actual NA object.
local mi,ma = NA{1,6,11,2,20}:sub(1,3):minmax()
assert(mi == 1 and ma == 11)
-- properties
local props = require 'ml_properties'
P = class()
function P:update (k,v)
last_set = k
end
-- any explicit setters will be called on construction
function P:set_name (name)
self.myname = name
end
function P:get_name ()
last_get = 'name'
return self.myname
end
-- have to call this after any setters or getters are defined...
props(P,{
__update = P.update; -- will be called after setting _props
enabled = true,
visible = false,
name = 'foo',
})
p = P()
asserteq (p,{myname="foo",_enabled=true,_visible=false})
assert(p.enabled==true and p.visible==false)
p.visible = true
asserteq(last_set,'visible')
p.name = 'boo'
assert (p.name == 'boo' and last_get == 'name')
================================================
FILE: microlight-1.1-1.rockspec
================================================
package = "microlight"
version = "1.1-1"
source = {
url = "git://github.com/stevedonovan/Microlight.git",
tag = "1.1",
dir = "."
}
description = {
summary = "A compact set of Lua utility functions.",
detailed = [[
Microlight provides a table stringifier, string spit and substitution,
useful table operations, basic class support and some functional helpers.
]],
homepage = "http://stevedonovan.github.com/microlight/",
license = "MIT/X11",
maintainer = "steve.j.donovan@gmail.com",
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "builtin",
modules = {
ml = "ml.lua" ,
}
}
================================================
FILE: ml.lua
================================================
-----------------
-- Microlight - a very compact Lua utilities module
--
-- Steve Donovan, 2012; License MIT
-- @module ml
local ml = {}
local select,pairs = select,pairs
local function_arg
table.unpack = table.unpack or unpack
---------------------------------------------------
-- String utilties.
-- @section string
---------------------------------------------------
--- split a delimited string into an array of strings.
-- @param s The input string
-- @param re A Lua string pattern; defaults to '%s+'
-- @param n optional maximum number of splits
-- @return an array of strings
function ml.split(s,re,n)
local find,sub,append = string.find, string.sub, table.insert
local i1,ls = 1,{}
if not re then re = '%s+' end
if re == '' then return {s} end
while true do
local i2,i3 = find(s,re,i1)
if not i2 then
local last = sub(s,i1)
if last ~= '' then append(ls,last) end
if #ls == 1 and ls[1] == '' then
return {}
else
return ls
end
end
append(ls,sub(s,i1,i2-1))
if n and #ls == n then
ls[#ls] = sub(s,i1)
return ls
end
i1 = i3+1
end
end
ml.lua51 = _VERSION:match '5%.1$'
--- escape any 'magic' pattern characters in a string.
-- Useful for functions like `string.gsub` and `string.match` which
-- always work with Lua string patterns.
-- For any s, `s:match('^'..escape(s)..'$') == s` is `true`.
-- @param s The input string
-- @return an escaped string
function ml.escape(s)
local res = s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
if ml.lua51 then
res = res:gsub('%z','%%z')
end
return res
end
--- expand a string containing any `${var}` or `$var`.
-- Substitution values should be only numbers or strings.
-- However, you should pick _either one_ consistently!
-- @param s the string
-- @param subst either a table or a function (as in `string.gsub`)
-- @return expanded string
function ml.expand (s,subst)
local res,k = s:gsub('%${([%w_]+)}',subst)
if k > 0 then return res end
return (res:gsub('%$([%w_]+)',subst))
end
--- return the contents of a file as a string
-- @param filename The file path
-- @param is_bin open in binary mode, default false
-- @return file contents, or nil,error
function ml.readfile(filename,is_bin)
local mode = is_bin and 'b' or ''
local f,err = io.open(filename,'r'..mode)
if not f then return nil,err end
local res,err = f:read('*a')
f:close()
if not res then return nil,err end
return res
end
--- write a string to a file,
-- @param filename The file path
-- @param str The string
-- @param is_bin open in binary mode, default false
-- @return true or nil,error
function ml.writefile(filename,str,is_bin)
local f,err = io.open(filename,'w'..(is_bin or ''))
if not f then return nil,err end
f:write(str)
f:close()
return true
end
---------------------------------------------------
-- File and Path functions
-- @section file
---------------------------------------------------
--- Does a file exist?
-- @param filename a file path
-- @return the file path, otherwise nil
-- @usage file = exists 'readme' or exists 'readme.txt' or exists 'readme.md'
function ml.exists (filename)
local f = io.open(filename)
if not f then
return nil
else
f:close()
return filename
end
end
local sep, other_sep = package.config:sub(1,1),'/'
--- split a path into directory and file part.
-- if there's no directory part, the first value will be the empty string.
-- Handles both forward and back-slashes on Windows.
-- @param P A file path
-- @return the directory part
-- @return the file part
function ml.splitpath(P)
local i = #P
local ch = P:sub(i,i)
while i > 0 and ch ~= sep and ch ~= other_sep do
i = i - 1
ch = P:sub(i,i)
end
if i == 0 then
return '',P
else
return P:sub(1,i-1), P:sub(i+1)
end
end
--- split a path into root and extension part.
-- if there's no extension part, the second value will be empty
-- @param P A file path
-- @return the name part
-- @return the extension
function ml.splitext(P)
local i = #P
local ch = P:sub(i,i)
while i > 0 and ch ~= '.' do
if ch == sep or ch == other_sep then
return P,''
end
i = i - 1
ch = P:sub(i,i)
end
if i == 0 then
return P,''
else
return P:sub(1,i-1),P:sub(i)
end
end
---------------------------------------------------
-- Extended table functions.
-- 'array' here is shorthand for 'array-like table'; these functions
-- only operate over the numeric `1..#t` range of a table and are
-- particularly efficient for this purpose.
-- @section table
---------------------------------------------------
local tostring = tostring -- so we can globally override tostring!
local function quote (v)
if type(v) == 'string' then
return ('%q'):format(v)
else
return tostring(v)
end
end
local lua_keyword = {
["and"] = true, ["break"] = true, ["do"] = true,
["else"] = true, ["elseif"] = true, ["end"] = true,
["false"] = true, ["for"] = true, ["function"] = true,
["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
["not"] = true, ["or"] = true, ["repeat"] = true,
["return"] = true, ["then"] = true, ["true"] = true,
["until"] = true, ["while"] = true, ["goto"] = true,
}
local function is_iden (key)
return key:match '^[%a_][%w_]*$' and not lua_keyword[key]
end
local tbuff
function tbuff (t,buff,k,start_indent,indent)
local start_indent2, indent2
if start_indent then
start_indent2 = indent
indent2 = indent .. indent
end
local function append (v)
if not v then return end
buff[k] = v
k = k + 1
end
local function put_item(value)
if type(value) == 'table' then
if not buff.tables[value] then
buff.tables[value] = true
k = tbuff(value,buff,k,start_indent2,indent2)
else
append("<cycle>")
end
else
value = quote(value)
append(value)
end
append ","
if start_indent then append '\n' end
end
append "{"
if start_indent then append '\n' end
-- array part -------
local array = {}
for i,value in ipairs(t) do
append(indent)
put_item(value)
array[i] = true
end
-- 'map' part ------
for key,value in pairs(t) do if not array[key] then
append(indent)
-- non-identifiers need ["key"]
if type(key)~='string' or not is_iden(key) then
if type(key)=='table' then
key = ml.tstring(key)
else
key = quote(key)
end
key = "["..key.."]"
end
append(key..'=')
put_item(value)
end end
-- removing trailing comma is done for prettiness, but this implementation
-- is not pretty at all!
local last = start_indent and buff[k-2] or buff[k-1]
if start_indent then
if last == '{' then -- empty table
k = k - 1
else
if last == ',' then -- get rid of trailing comma
k = k - 2
append '\n'
end
append(start_indent)
end
elseif last == "," then -- get rid of trailing comma
k = k - 1
end
append "}"
return k
end
--- return a string representation of a Lua value.
-- Cycles are detected, and the result can be optionally indented nicely.
-- @param t the table
-- @param how (optional) a table with fields `spacing' and 'indent', or a string corresponding
-- to `indent`.
-- @return a string
function ml.tstring (t,how)
if type(t) == 'table' and not (getmetatable(t) and getmetatable(t).__tostring) then
local buff = {tables={[t]=true}}
how = how or {}
if type(how) == 'string' then how = {indent = how} end
pcall(tbuff,t,buff,1,how.spacing or how.indent,how.indent)
return table.concat(buff)
else
return quote(t)
end
end
local append = table.insert
--- collect a series of values from an interator.
-- @param ... iterator
-- @return array-like table
-- @usage collect(pairs(t)) is the same as keys(t)
function ml.collect (...)
local res = {}
for k in ... do append(res,k) end
return res
end
--- collect from an interator up to a condition.
-- If the function returns true, then collection stops.
-- @param f predicate receiving (value,count)
-- @param ... iterator
-- @return array-like table
function ml.collectuntil (f,...)
local res,i,pred = {},1,function_arg(f)
for k in ... do
if pred(k,i) then break end
res[i] = k
i = i + 1
end
return res
end
--- collect `n` values from an interator.
-- @param n number of values to collect
-- @param ... iterator
-- @return array-like table
function ml.collectn (n,...)
return collectuntil(function(k,i) return i > n end,...)
end
--- collect the second value from a iterator.
-- If the second value is `nil`, it won't be collected!
-- @param ... iterator
-- @return array-like table
-- @usage collect2nd(pairs{one=1,two=2}) is {1,2} or {2,1}
function ml.collect2nd (...)
local res = {}
for _,v in ... do append(res,v) end
return res
end
--- extend a table by mapping a function over another table.
-- @param dest destination table
-- @param j start index in destination
-- @param nilv default value to use if function returns `nil`
-- @param f the function
-- @param t source table
-- @param ... extra arguments to function
function ml.mapextend (dest,j,nilv,f,t,...)
f = function_arg(f)
if j == -1 then j = #dest + 1 end
for i = 1,#t do
local val = f(t[i],...)
val = val~=nil and val or nilv
if val ~= nil then
dest[j] = val
j = j + 1
end
end
return dest
end
local mapextend = ml.mapextend
--- map a function over an array.
-- The output must always be the same length as the input, so
-- any `nil` values are mapped to `false`.
-- @param f a function of one or more arguments
-- @param t the array
-- @param ... any extra arguments to the function
-- @return a new array with elements `f(t[i],...)`
function ml.imap(f,t,...)
return mapextend({},1,false,f,t,...)
end
--- apply a function to each element of an array.
-- @param f a function of one or more arguments
-- @param t the array
-- @param ... any extra arguments to the function
-- @return the transformed array
function ml.transform (f,t,...)
return mapextend(t,1,false,f,t,...)
end
--- map a function over values from two arrays.
-- Length of output is the size of the smallest array.
-- @param f a function of two or more arguments
-- @param t1 first array
-- @param t2 second array
-- @param ... any extra arguments to the function
-- @return a new array with elements `f(t1[i],t2[i],...)`
function ml.imap2(f,t1,t2,...)
f = function_arg(f)
local res = {}
local n = math.min(#t1,#t2)
for i = 1,n do
res[i] = f(t1[i],t2[i],...) or false
end
return res
end
--- map a function over an array only keeping non-`nil` values.
-- @param f a function of one or more arguments
-- @param t the array
-- @param ... any extra arguments to the function
-- @return a new array with elements `v = f(t[i],...) such that v ~= nil`
function ml.imapfilter (f,t,...)
return mapextend({},1,nil,f,t,...)
end
--- filter an array using a predicate.
-- @param t a table
-- @param pred a function that must return `nil` or `false`
-- to exclude a value
-- @param ... any extra arguments to the predicate
-- @return a new array such that `pred(t[i])` evaluates as true
function ml.ifilter(t,pred,...)
local res,k = {},1
pred = function_arg(pred)
for i = 1,#t do
if pred(t[i],...) then
res[k] = t[i]
k = k + 1
end
end
return res
end
--- find an item in an array using a predicate.
-- @param t the array
-- @param pred a function of at least one argument
-- @param ... any extra arguments
-- @return the item value, or `nil`
-- @usage ifind({{1,2},{4,5}},'X[1]==Y',4) is {4,5}
function ml.ifind(t,pred,...)
pred = function_arg(pred)
for i = 1,#t do
if pred(t[i],...) then
return t[i]
end
end
end
--- return the index of an item in an array.
-- @param t the array
-- @param value item value
-- @param cmp optional comparison function (default is `X==Y`)
-- @return index, otherwise `nil`
function ml.indexof (t,value,cmp)
if cmp then cmp = function_arg(cmp) end
for i = 1,#t do
local v = t[i]
if cmp and cmp(v,value) or v == value then
return i
end
end
end
local function upper (t,i2)
if not i2 or i2 > #t then
return #t
elseif i2 < 0 then
return #t + i2 + 1
else
return i2
end
end
local function copy_range (dest,index,src,i1,i2)
local k = index
for i = i1,i2 do
dest[k] = src[i]
k = k + 1
end
return dest
end
--- return a slice of an array.
-- Like `string.sub`, the end index may be negative.
-- @param t the array
-- @param i1 the start index, default 1
-- @param i2 the end index, default #t
-- @return an array of `t[i]` for `i` from `i1` to `i2` inclusive
function ml.sub(t,i1,i2)
i1, i2 = i1 or 1, upper(t,i2)
return copy_range({},1,t,i1,i2)
end
--- delete a range of values from an array.
-- @param tbl the array
-- @param start start index
-- @param finish end index (like `ml.sub`)
function ml.removerange(tbl,start,finish)
finish = upper(tbl,finish)
local count = finish - start + 1
for k=start+count,#tbl do tbl[k-count]=tbl[k] end
for k=#tbl,#tbl-count+1,-1 do tbl[k]=nil end
end
--- copy values from `src` into `dest` starting at `index`.
-- By default, it moves up elements of `dest` to make room.
-- @param dest destination array
-- @param index start index in destination
-- @param src source array
-- @param overwrite write over values
function ml.insertvalues(dest,index,src,overwrite)
local sz = #src
if not overwrite then
for i = #dest,index,-1 do dest[i+sz] = dest[i] end
end
copy_range(dest,index,src,1,sz)
end
--- extend an array using values from other tables.
-- @{readme.md.Extracting_and_Mapping}
-- @param t the array to be extended
-- @param ... the other arrays
-- @return the extended array
function ml.extend(t,...)
for i = 1,select('#',...) do
ml.insertvalues(t,#t+1,select(i,...),true)
end
return t
end
--- make an array of indexed values.
-- Generalized table indexing. Result will only contain
-- values for keys that exist.
-- @param t a table
-- @param keys an array of keys or indices
-- @return an array `L` such that `L[keys[i]]`
-- @usage indexby({one=1,two=2},{'one','three'}) is {1}
-- @usage indexby({10,20,30,40},{2,4}) is {20,40}
function ml.indexby(t,keys)
local res = {}
for _,v in pairs(keys) do
if t[v] ~= nil then
append(res,t[v])
end
end
return res
end
--- create an array of numbers from start to end.
-- With one argument it goes `1..x1`. `d` may be a
-- floating-point fraction
-- @param x1 start value
-- @param x2 end value
-- @param d increment (default 1)
-- @return array of numbers
-- @usage range(2,10) is {2,3,4,5,6,7,8,9,10}
-- @usage range(5) is {1,2,3,4,5}
function ml.range (x1,x2,d)
if not x2 then
x2 = x1
x1 = 1
end
d = d or 1
local res,k = {},1
for x = x1,x2,d do
res[k] = x
k = k + 1
end
return res
end
-- Bring modules or tables into 't`.
-- If `lib` is a string, then it becomes the result of `require`
-- With only one argument, the second argument is assumed to be
-- the `ml` table itself.
-- @param t table to be updated, or current environment
-- @param lib table, module name or `nil` for importing 'ml'
-- @return the updated table
function ml.import(t,...)
local other
-- explicit table, or current environment
-- this isn't quite right - we won't get the calling module's _ENV
-- this way. But it does prevent execution of the not-implemented setfenv.
t = t or _ENV or getfenv(2)
local libs = {}
if select('#',...)==0 then -- default is to pull in this library!
libs[1] = ml
else
for i = 1,select('#',...) do
local lib = select(i,...)
if type(lib) == 'string' then
local value = _G[lib]
if not value then -- lazy require!
value = require (lib)
-- and use the module part of package for the key
lib = lib:match '[%w_]+$'
end
lib = {[lib]=value}
end
libs[i] = lib
end
end
return ml.update(t,table.unpack(libs))
end
--- add the key/value pairs of arrays to the first array.
-- For sets, this is their union. For the same keys,
-- the values from the first table will be overwritten.
-- @param t table to be updated
-- @param ... tables containg more pairs to be added
-- @return the updated table
function ml.update (t,...)
for i = 1,select('#',...) do
for k,v in pairs(select(i,...)) do
t[k] = v
end
end
return t
end
--- make a table from an array of keys and an array of values.
-- @param t an array of keys
-- @param tv an array of values
-- @return a table where `{[t[i]]=tv[i]}`
-- @usage makemap({'power','glory'},{20,30}) is {power=20,glory=30}
function ml.makemap(t,tv)
local res = {}
for i = 1,#t do
res[t[i]] = tv and tv[i] or i
end
return res
end
--- make a set from an array.
-- The values are the original array indices.
-- @param t an array of values
-- @return a table where the keys are the indices in the array.
-- @usage invert{'one','two'} is {one=1,two=2}
-- @function ml.invert
ml.invert = ml.makemap
--- extract the keys of a table as an array.
-- @param t a table
-- @return an array of keys
function ml.keys(t)
return ml.collect(pairs(t))
end
--- are all the values of `other` in `t`?
-- @param t a set
-- @param other a possible subset
-- @treturn bool
function ml.issubset(t,other)
for k,v in pairs(other) do
if t[k] == nil then return false end
end
return true
end
--- are all the keys of `other` in `t`?
-- @param t a table
-- @param other another table
-- @treturn bool
ml.containskeys = ml.issubset
--- return the number of keys in this table, or members in this set.
-- @param t a table
-- @treturn int key count
function ml.count (t)
local count = 0
for k in pairs(t) do count = count + 1 end
return count
end
--- do these tables have the same keys?
-- THis is set equality.
-- @param t a table
-- @param other a table
-- @return true or false
function ml.equalkeys(t,other)
return ml.issubset(t,other) and ml.issubset(other,t)
end
---------------------------------------------------
-- Functional helpers.
-- @section function
---------------------------------------------------
--- create a function which will throw an error on failure.
-- @param f a function that returns nil,err if it fails
-- @return an equivalent function that raises an error
function ml.throw(f)
f = function_arg(f)
return function(...)
local r1,r2,r3 = f(...)
if not r1 then error(r2,2) end
return r1,r2,r3
end
end
--- bind the value `v` to the first argument of function `f`.
-- @param f a function of at least one argument
-- @param v a value
-- @return a function of one less argument
-- @usage (bind1(string.match,'hello')('^hell') == 'hell'
function ml.bind1(f,v)
f = function_arg(f)
return function(...)
return f(v,...)
end
end
--- bind the value `v` to the second argument of function `f`.
-- @param f a function of at least one argument
-- @param v a value
-- @return a function of one less argument
-- @usage (bind2(string.match,'^hell')('hello') == 'hell'
function ml.bind2(f,v)
f = function_arg(f)
return function(x,...)
return f(x,v,...)
end
end
--- compose two functions.
-- For instance, `printf` can be defined as `compose(io.write,string.format)`
-- @param f1 a function
-- @param f2 a function
-- @return `f1(f2(...))`
function ml.compose(f1,f2)
f1 = function_arg(f1)
f2 = function_arg(f2)
return function(...)
return f1(f2(...))
end
end
--- a function returning the second value of `f`
-- @param f a function returning at least two values
-- @return a function returning second of those values
-- @usage take2(splitpath) is basename
function ml.take2 (f)
f = function_arg(f)
return function(...)
local _,b = f(...)
return b
end
end
--- is the object either a function or a callable object?.
-- @param obj Object to check.
-- @return true if callable
function ml.callable (obj)
return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call
end
--- create a callable from an indexable object.
-- @param t a table or other indexable object.
function ml.map2fun (t)
return setmetatable({},{
__call = function(obj,key) return t[key] end
})
end
--- create an indexable object from a callable.
-- @param f a callable of one argument.
function ml.fun2map (f)
return setmetatable({},{
__index = function(obj,key) return f(key) end;
__newindex = function() error("not writeable!",2) end
})
end
local function _string_lambda (f)
local code = 'return function(X,Y,Z) return '..f..' end'
local chunk = assert(loadstring(code,'tmp'))
return chunk()
end
local string_lambda
--- defines how we convert something to a callable.
--
-- Currently, anything that matches @{callable} or is a _string lambda_.
-- These are expressions with any of the placeholders, `X`,`Y` or `Z`
-- corresponding to the first, second or third argument to the function.
--
-- This can be overriden by people
-- wishing to extend the idea of 'callable' in this library.
-- @param f a callable or a string lambda.
-- @return a function
-- @raise error if `f` is not callable in any way, or errors in string lambda.
-- @usage function_arg('X+Y')(1,2) == 3
function ml.function_arg(f)
if type(f) == 'string' then
if not string_lambda then
string_lambda = ml.memoize(_string_lambda)
end
f = string_lambda(f)
else
assert(ml.callable(f),"expecting a function or callable object")
end
return f
end
function_arg = ml.function_arg
--- 'memoize' a function (cache returned value for next call).
-- This is useful if you have a function which is relatively expensive,
-- but you don't know in advance what values will be required, so
-- building a table upfront is wasteful/impossible.
-- @param func a function of at least one argument
-- @return a function with at least one argument, which is used as the key.
function ml.memoize(func)
return setmetatable({}, {
__index = function(self, k, ...)
local v = func(k,...)
self[k] = v
return v
end,
__call = function(self, k) return self[k] end
})
end
---------------------------------------------------
-- Classes.
-- @section class
---------------------------------------------------
--- create a class with an optional base class.
--
-- See @{readme.md.Classes}
-- The resulting table can be called to make a new object, which invokes
-- an optional constructor named `_init`. If the base
-- class has a constructor, you can call it as the `super()` method.
-- Every class has a `_class` and a maybe-nil `_base` field, which can
-- be accessed through the object.
--
-- All metamethods are inherited.
-- The class is given a function `Klass.classof(obj)`.
-- @param base optional base class
-- @return the callable metatable representing the class
function ml.class(base)
local klass, base_ctor = {}
if base then
ml.import(klass,base)
klass._base = base
base_ctor = rawget(base,'_init')
end
klass.__index = klass
klass._class = klass
klass.classof = function(obj)
local m = getmetatable(obj) -- an object created by class() ?
if not m or not m._class then return false end
while m do -- follow the inheritance chain --
if m == klass then return true end
m = rawget(m,'_base')
end
return false
end
setmetatable(klass,{
__call = function(klass,...)
local obj = setmetatable({},klass)
if rawget(klass,'_init') then
klass.super = base_ctor
local res = klass._init(obj,...) -- call our constructor
if res then -- which can return a new self..
obj = setmetatable(res,klass)
end
elseif base_ctor then -- call base ctor automatically
base_ctor(obj,...)
end
return obj
end
})
return klass
end
------------------------
-- a simple Array class.
-- @{readme.md.Array_Class}
--
-- `table` functions: `sort`,`concat`,`insert`,`remove`,`insert` as `append`.
--
-- `ml` functions: `ifilter` as `filter`,`imap` as `map`,`sub`,`indexby`,`range`,
-- `indexof`,`ifind` as `find`,`extend`,`split` and `collect`.
--
-- The `sorted` method returns a sorted copy.
--
-- Concatenation, equality and custom tostring is defined.
--
-- This implementation has covariant methods; so that methods like `map` and `sub`
-- will return an object of the derived type, not `Array`
-- @table Array
local Array
if not rawget(_G,'NO_MICROLIGHT_ARRAY') then
Array = ml.class()
local extend, setmetatable, C = ml.extend, setmetatable, ml.compose
local function set_class (self,res)
return setmetatable(res,self._class)
end
local function awrap (fun)
return function(self,...) return set_class(self,fun(self,...)) end
end
local function awraps (fun)
return function(self,f,...) return set_class(self,fun(f,self,...)) end
end
-- a class is just a table of functions, so we can do wholesale updates!
ml.import(Array,{
-- straight from the table library
concat=table.concat,insert=table.insert,remove=table.remove,append=table.insert,
-- originals return table; these versions make the tables into arrays.
filter=awrap(ml.ifilter),sub=awrap(ml.sub), indexby=awrap(ml.indexby),
map=awraps(ml.imap), map2=awraps(ml.imap2), mapfilter=awraps(ml.imapfilter),
range=C(Array,ml.range),split=C(Array,ml.split),collect=C(Array,ml.collect),
indexof=ml.indexof, find=ml.ifind, extend=ml.extend
})
-- A constructor can return a _specific_ object
function Array:_init(t)
if not t then return nil end -- no table, make a new one
if t._class == self._class then -- was already a Array: copy constructor!
t = ml.sub(t,1)
end
return t
end
function Array:sort(f)
if type(f) ~= "nil" then f = function_arg(f) end
table.sort(self,f)
return self
end
function Array:sorted(f)
return self:sub(1):sort(f)
end
function Array:foreach(f,...)
f = function_arg(f)
for i = 1,#self do f(self[i],...) end
end
function Array.mappers (klass,t)
local method = Array.mapfilter
if t.__use then
method = t.__use
t.__use = nil
end
for k,f in pairs(t) do
klass[k] = ml.bind2(method,function_arg(f))
end
end
function Array:__tostring()
return '{' .. self:map(ml.tstring):concat ',' .. '}'
end
function Array.__eq(l1,l2)
if #l1 ~= #l2 then return false end
for i = 1,#l1 do
if l1[i] ~= l2[i] then return false end
end
return true
end
function Array.__concat (l1,l2)
return set_class(l1,extend({},l1,l2))
end
end
ml.Array = Array
return ml
================================================
FILE: ml_module.lua
================================================
---------
-- Simple Lua 5.2 module.
-- Thin wrapper around ml.import
--
-- local _ENV = require 'ml_module' (_G)
-- function f1() .. end
-- function f2() .. end
-- return _M
--
-- See mod52.lua for an example of usage
-- @module ml_module
local ml = require 'ml'
return function(G,...)
local _M, EMT = {}, {}
local env = {_M=_M} --> this will become _ENV
ml.import(env,...)
EMT.__newindex = function(t,k,v)
rawset(env,k,v) -- copy to environment
_M[k] = v -- and add to module!
end
-- any undefined lookup goes to 'global' table specified
if G ~= nil then EMT.__index = G end
return setmetatable(env,EMT)
end
================================================
FILE: ml_properties.lua
================================================
--- set properties table for an existing class.
-- A property P can be fully specified by the class having
-- 'get_P' and 'set_P' methods. If only the setter is specified,
-- then accessing P acesses a private variable '_P'. If no
-- setters are specified, then the class can be notified of any
-- changes by defining an update() method and setting the special field
-- `__update` of `props` to that method.
-- `klass` is a class generated by 'ml.class`
-- `props` property definitions. This assigns each property
-- to a default value.
-- @module ml_properties
local ml = require 'ml'
return function (klass,props)
local setters,getters,_props,_names,_defs = {},{},{},{},{}
local rawget = rawget
local update = props.__update
props.__update = nil
for k,t in pairs(props) do
getters[k] = rawget(klass,'get_'..k)
if not getters[k] then
_props['_'..k] = t
_names[k] = '_'..k
end
setters[k] = rawget(klass,'set_'..k)
if setters[k] then
_defs[k] = t
end
end
klass._props = props
-- patch the constructor so it sets property default values
local kmt = getmetatable(klass)
local ctor = kmt.__call
kmt.__call = function(...)
local newi = klass.__newindex
klass.__newindex = nil
local obj = ctor(...)
ml.import(obj,_props)
for k,set in pairs(setters) do
set(obj,_defs[k])
end
klass.__newindex = newi
return obj
end
klass.__index = function(t,k)
local v = rawget(klass,k)
if v then return v end
local getter = getters[k]
if getter then
return getter(t,k)
else
local _name = _names[k]
if _name then return t[_name]
else error("unknown readable property: "..k,2)
end
end
end
klass.__newindex = function(t,k,v)
local setter = setters[k]
if setter then
setter(t,v,k)
else
local _name = _names[k]
if _name then
t[_name] = v
if update then update(t,k,v) end
else error("unknown writeable property: "..k,2)
end
end
end
end
================================================
FILE: ml_range.lua
================================================
-- ml_range.lua (c) 2012 Dirk Laurie, Lua-like MIT licence, except that
-- Steve Donovan is allowed to use the code in any way he likes.
--[[
Usage:
range = require "ml_range"
`range(n)` returns {1,2,3,...,n}, with vector semantics. All binary
operations are term-by-term, with numbers allowed left or right.
Exponentiation and logical operators are undefined at this stage.
Sum and product methods, with optional starting values, are supplied.
The vector class could be useful independently. To gain access to it:
Vector = getmetatable(range(1))
Then e.g. Vector{1,2,3,4} creates a vector.
Some fun can be had with
debug.setmetatable(1,{__len=range})
e.g.
print(2*#10-1) --> {1,3,5,7,9,11,13,15,17,19}
]]
local concat = table.concat
local isnumber = function(x) return type(x)=='number' end
local instance = function (class,obj) return setmetatable(obj,class) end
local Vector
Vector = {
__unm = function(x) local s=Vector{}
for k=1,#x do s[k]=-x[k] end
return s
end;
__add = function(x,y) local s=Vector{}
if isnumber(x) then for k=1,#y do s[k]=x+y[k] end
elseif isnumber(y) then for k=1,#x do s[k]=x[k]+y end
else for k=1,#x do s[k]=x[k]+y[k] end
end
return s
end;
__sub = function(x,y) local s=Vector{}
if isnumber(x) then for k=1,#y do s[k]=x-y[k] end
elseif isnumber(y) then for k=1,#x do s[k]=x[k]-y end
else for k=1,#x do s[k]=x[k]-y[k] end
end
return s
end;
__mul = function(x,y) local s=Vector{}
if isnumber(x) then for k=1,#y do s[k]=x*y[k] end
elseif isnumber(y) then for k=1,#x do s[k]=x[k]*y end
else for k=1,#x do s[k]=x[k]*y[k] end
end
return s
end;
__div = function(x,y) local s=Vector{}
if isnumber(x) then for k=1,#y do s[k]=x/y[k] end
elseif isnumber(y) then for k=1,#x do s[k]=x[k]/y end
else for k=1,#x do s[k]=x[k]/y[k] end
end
return s
end;
__concat = function(x,y) local s=Vector{}
for k,v in ipairs(x) do s[k]=v end
if isnumber(x) then insert(s,1,x)
elseif isnumber(y) then append(s,x)
else for k,v in ipairs(y) do s[#s+1]=v end
end
return s
end;
__tostring = function(x) return '{'..concat(x,',')..'}'
end;
sum = function(x,y) local sum=y or 0
for k,v in ipairs(x) do sum = sum+v end
return sum
end;
prod = function(x,y) local prod=y or 1
for k,v in ipairs(x) do prod = prod*v end
return prod
end;
}
setmetatable(Vector,{__call = instance})
Vector.__index = Vector
local function range(x)
local s=Vector{}
for k=1,x do s[k]=k end
return s
end
return range
================================================
FILE: mod52.lua
================================================
local _ENV = require 'ml_module' (nil, -- no wholesale access to _G
'print','assert','os', -- quoted global values brought in
'lfs', -- not global, so use require()!
table -- not quoted, import the whole table into the environment!
)
function format (s)
local out = {'Hello',s,'at',os.date('%c'),'here is',lfs.currentdir()}
return concat(out,' ')
end
function message(s)
print(format(s))
end
-- no, we didn't bring anything else in
assert(setmetatable == nil)
-- NB return the _module_, not the _environment_!
return _M
================================================
FILE: readme.md
================================================
# A Small but Useful Lua library
The Lua standard library is deliberately kept small, based on the abstract platform
defined by the C89 standard. It is intended as a base for further development, so Lua
programmers tend to collect small useful functions for their projects.
Microlight is an attempt at 'library golf', by analogy to the popular nerd sport 'code
golf'. The idea here is to try capture some of these functions in one place and document
them well enough so that it is easier to use them than to write them yourself.
This library is intended to be a 'extra light' version of Penlight, which has nearly two
dozen modules and hundreds of functions.
In Lua, anything beyond the core involves 'personal' choice, and this list of functions
does not claim to aspire to 'canonical' status. It emerged from discussion on the Lua
Mailing list started by Jay Carlson, and was implemented by myself and Dirk Laurie.
## Strings
THere is no built-in way to show a text representation of a Lua table, which can be
frustrating for people first using the interactive prompt. Microlight provides `tstring`.
Please note that globally redefining `tostring` is _not_ a good idea for Lua application
development! This trick is intended to make experimation more satisfying:
> require 'ml'.import()
> tostring = tstring
> = {10,20,name='joe'}
{10,20,name="joe"}
The Lua string functions are particularly powerful but there are some common functions
missing that tend to come up in projects frequently. There is `table.concat` for building
a string out of a table, but no `table.split` to break a string into a table.
> = split('hello dolly')
{"hello","dolly"}
> = split('one,two',',')
{"one","two"}
The second argument is a _string pattern_ that defaults to spaces.
Although it's not difficult to do [string
interpolation](http://lua-users.org/wiki/StringInterpolation) in Lua, there's no little
function to do it directly. So Microlight provides `ml.expand`.
> = expand("hello $you, from $me",{you='dolly',me='joe'})
hello dolly, from joe
`expand` also understands the alternative `${var}` and may also be given a function, just
like `string.gsub`. (But pick one _or_ the other consistently.)
Lua string functions match using string patterns, which are a powerful subset of proper
regular expressions: they contain 'magic' characters like '.','$' etc which you need to
escape before using. `escape` is used when you wish to match a string literally:
> = ('woo%'):gsub(escape('%'),'hoo')
"woohoo" 1
> = split("1.2.3",escape("."))
{"1","2","3"}
## Files and Paths
Although `access` is available on most platforms, it's not part of the standard, (which
is why it's spelt `_access` on Windows). So to test for the existance of a file, you need
to attempt to open it. So the `exist` function is easy to write:
function ml.exists (filename)
local f = io.open(filename)
if not f then
return nil
else
f:close()
return filename
end
end
The return value is _not_ a simple true or false; it returns the filename if it exists so
we can easily find an existing file out of a group of candidates:
> = exists 'README' or exists 'readme.txt' or exists 'readme.md'
"readme.md"
Lua is good at slicing and dicing text, so a common strategy is to read all of a
not-so-big file and process the string. This is the job of `readfile`. For instance, this
returns the first 128 bytes of the file opened in binary mode:
> txt = readfile('readme.md',true):sub(1,128)
Note I said bytes, not characters, since strings can contain any byte sequence.
If `readfile` can't open a file, or can't read from it, it will return `nil` and an error
message. This is the pattern followed by `io.open` and many other Lua functions; it is
considered bad form to raise an error for a _routine_ problem.
Breaking up paths into their components is done with `splitpath` and `splitext`:
> = splitpath(path)
"/path/to/dogs" "bonzo.txt"
> = splitext(path)
"/path/to/dogs/bonzo" ".txt"
> = splitpath 'frodo.txt'
"" "frodo.txt"
> = splitpath '/usr/'
"/usr" ""
> = splitext '/usr/bin/lua'
"/usr/bin/lua" ""
>
These functions return _two_ strings, one of which may be the empty string (rather than
`nil`). On Windows, they use both forward- and back-slashes, on Unix only forward slashes.
## Inserting and Extending
Most of the Microlight functions work on Lua tables. Although these may be _both_ arrays
_and_ hashmaps, generally we tend to _use_ them as one or the other. From now on, we'll
use array and map as shorthand terms for tables
`update` adds key/value pairs to a map, and `extend` appends an array to an array; they
are two complementary ways to add multiple items to a table in a single operation.
> a = {one=1,two=2}
> update(a,{three=3,four=4})
> = a
{one=1,four=4,three=3,two=2}
> t = {10,20,30}
> extend(t,{40,50})
> = t
{10,20,30,40,50}
As from version 1.1, both of these functions take an arbitrary number of tables.
To 'flatten' a table, just unpack it and use `extend`:
> pair = {{1,2},{3,4}}
> = extend({},unpack(pair))
{1,2,3,4}
`extend({},t)` would just be a shallow copy of a table.
More precisely, `extend` takes an indexable and writeable object, where the index
runs from 1 to `#O` with no holes, and starts adding new elements at `O[#O+1]`.
Simularly, the other arguments are indexable but need not be writeable. These objects
are typically tables, but don't need to be. You can exploit the guarantee that `extend`
always goes sequentially from 1 to `#T`, and make the first argument an object:
> obj = setmetatable({},{ __newindex = function(t,k,v) print(v) end })
> extend(obj,{1,2,3})
1
2
3
To insert multiple values into a position within an array, use `insertvalues`. It works
like `table.insert`, except that the third argument is an array of values. If you do want
to overwrite values, then use `true` for the fourth argument:
> t = {10,20,30,40,50}
> insertvalues(t,2,{11,12})
> = t
{10,11,12,20,30,40,50}
> insertvalues(t,3,{2,3},true)
> = t
{10,11,2,3,30,40,50}
(Please note that the _original_ table is modified by these functions.)
`update' works like `extend`. except that all the key value pairs from the input tables
are copied into the first argument. Keys may be overwritten by subsequent tables.
> t = {}
> update(t,{one=1},{ein=1},{one='ONE'})
> = t
{one="ONE",ein=1}
`import` is a specialized version of `update`; if the first argument is `nil` then it's
assumed to be the global table. If no tables are provided, it brings in the ml table
itself (hence the lazy `require "ml".import()` idiom).
If the arguments are strings, then we try to `require` them. So this brings in
LuaFileSystem and imports `lfs` into the global table. So it's a lazy way to do a whole
bunch of requires. A module 'package.mod' will be brought in as `mod`. Note that the
second form actually does bring all of `lpeg`'s functions in.
> import(nil,'lfs')
> import(nil,require 'lpeg')
## Extracting and Mapping
The opposite operation to extending is extracting a number of items from a table.
There's `sub`, which works just like `string.sub` and is the equivalent of list slicing
in Python:
> numbers = {10,20,30,40,50}
> = sub(numbers,1,1)
{10}
> = sub(numbers,2)
{20,30,40,50}
> = sub(numbers,1,-2)
{10,20,30,40}
`indexby` indexes a table by an array of keys:
> = indexby(numbers,{1,4})
{10,40}
> = indexby({one=1,two=2,three=3},{'three','two'})
{[3,2}
Here is the old standby `imap`, which makes a _new_ array by applying a function to the
original elements:
> words = {'one','two','three'}
> = imap(string.upper,words)
{"ONE","TWO","THREE"}
> s = {'10','x','20'}
> ns = imap(tonumber,s)
> = ns
{10,false,20}
`imap` must always return an array of the same size - if the function returns `nil`, then
we avoid leaving a hole in the array by using `false` as a placeholder.
Another popular function `indexof` does a linear search for a value and returns the
1-based index, or `nil` if not successful:
> = indexof(numbers,20)
2
> = indexof(numbers,234)
nil
This function takes an optional third argument, which is a custom equality function.
In general, you want to match something more than just equality. `ifind` will return the
first value that satisfies the given function.
> s = {'x','10','20','y'}
> = ifind(s,tonumber)
"10"
The standard function `tonumber` returns a non-nil value, so the corresponding value is
returned - that is, the string. To get all the values that match, use `ifilter`:
> = ifilter(numbers,tonumber)
{"10","20"}
There is a useful hybrid between `imap` and `ifilter` called `imapfilter` which is
particularly suited to Lua use, where a function commonly returns either something useful,
or nothing. (Phillip Janda originally suggested calling this `transmogrify`, since
no-one has preconceptions about it, except that it's a cool toy for imaginative boys).
> = imapfilter(tonumber,{'one',1,'f',23,2})
{1,23,2}
`collect` makes a array out of an iterator. 'collectuntil` can be given a
custom predicate and `collectn` takes up to a maximum number of values,
which is useful for iterators that never terminate.
(Note that we need to pass it either a proper iterator, like `pairs`, or
a function or exactly one function - which isn't the case with `math.random`)
> s = 'my dog ate your homework'
> words = collect(s:gmatch '%a+')
> = words
{"my","dog","ate","your","homework"}
> R = function() return math.random() end
> = collectn(3,R)
{0.0012512588885159,0.56358531449324,0.19330423902097}
> lines = collectuntil(4,io.lines())
one
two
three
four
> = lines
{"one","two","three","four"}
A simple utility to sort standard input looks like this:
require 'ml'.import()
lines = collect(io.lines())
table.sort(lines)
print(table.concat(lines,'\n'))
Another standard function that can be used here is `string.gmatch`.
LuaFileSystem defines an iterator over directory contents. `collect(lfs.dir(D))` gives
you an _array_ of all files in directory `D`.
Finally, `removerange` removes a _range_ of values from an array, and takes the same
arguments as `sub`. Unlike the filters filters, it works in-place.
## Sets and Maps
`indexof` is not going to be your tool of choice for really big tables, since it does a
linear search. Lookup on Lua hash tables is faster, if we can get the data into the right
shape. `invert` turns a array of values into a table with those values as keys:
> m = invert(numbers)
> = m
{[20]=2,[10]=1,[40]=4,[30]=3,[50]=5}
> = m[20]
2
> = m[30]
3
> = m[25]
nil
> m = invert(words)
> = m
{one=1,three=3,two=2}
So from a array we get a reverse lookup map. This is also exactly what we want from a
_set_: fast membership test and unique values.
Sets don't particularly care about the actual value, as long as it evaluates as true or
false, hence:
> = issubset(m,{one=true,two=true})
true
`makemap` takes another argument and makes up a table where the keys come from the first
array and the values from the second array:
> = makemap({'a','b','c'},{1,2,3})
{a=1,c=3,b=2}
# Higher-order Functions
Functions are first-class values in Lua, so functions may manipulate them, often called
'higher-order' functions.
By _callable_ we either mean a function or an object which has a `__call` metamethod. The
`callable` function checks for this case.
Function _composition_ is often useful:
> printf = compose(io.write,string.format)
> printf("the answer is %d\n",42)
the answer is 42
`bind1` and `bind2` specialize functions by creating a version that has one less
argument. `bind1` gives a function where the first argument is _bound_ to some value.
This can be used to pass methods to functions expecting a plain function. In Lua,
`obj:f()` is shorthand for `obj.f(obj,...)`. Just using a dot is not enough, since there
is no _implicit binding_ of the self argument. This is precisely what `bind1` can do:
> ewrite = bind1(io.stderr.write,io.stderr)
> ewrite 'hello\n'
We want a logging function that writes a message to standard error with a line feed; just
bind the second argument to '\n':
> log = bind2(ewrite,'\n')
> log 'hello'
hello
Note that `sub(t,1)` does a simple array copy:
> copy = bind2(sub,1)
> t = {1,2,3}
> = copy(t)
{1,2,3}
It's easy to make a 'predicate' for detecting empty or blank strings:
> blank = bind2(string.match,'^%s*$')
> = blank ''
""
> = blank ' '
" "
> = blank 'oy vey'
nil
I put 'predicate' in quotes because it's again not classic true/false; Lua actually only
developed `false` fairly late in its career. Operationally, this is a fine predicate
because `nil` matches as 'false' and any string matches as 'true'.
This pattern generates a whole family of classification functions, e.g. `hex` (using
'%x+'), `upcase` ('%u+'), `iden` ('%a[%w_]*') and so forth. You can keep the binding game
going (after all, `bind2` is just a function like any other.)
> matcher = bind1(bind2,string.match)
> hex = matcher '^%x+$'
Predicates are particularly useful for `ifind` and `ifilter`. It's now easy to filter
out strings from a array that match `blank` or `hex`, for instance.
It is not uncommon for Lua functions to return multiple useful values; sometimes the one
you want is the second value - this is what `take2` does:
> p = lfs.currentdir()
> = p
"C:\\Users\\steve\\lua\\Microlight"
> = splitpath(p)
"C:\\Users\\steve\\lua" "Microlight"
> basename = take2(splitpath)
> = basename(p)
"Microlight"
> extension = take2(splitext)
> = extension 'bonzo.dog'
".dog"
There is a pair of functions `map2fun` and `fun2map` which convert indexable objects into
callables and vice versa. Say I have a table of key/value pairs, but an API requires a
function - use `map2fun`. Alternatively, the API might want a lookup table and you only
have a lookup function. Say we have an array of objects with a name field. The `find`
method will give us an object with a particular name:
> obj = objects:find ('X.name=Y','Alfred')
{name='Afred',age=23}
> by_name = function(name) return objects:find('X.name=Y',name) end
> lookup = fun2map(by_name)
> = lookup.Alfred
{name='Alfred',age=23}
Now if you felt particularly clever and/or sadistic, that anonymous function could be
written like so: (note the different quotes needed to get a nested string lambda):
by_name = bind1('X:find("X.name==Y",Y)',objects)
## Classes
Lua and Javascript have two important things in common; objects are associative arrays,
with sugar so that `t.key == t['key']`; there is no built-in class mechanism. This causes
a lot of (iniital) unhappiness. It's straightforward to build a class system, and so it
is reinvented numerous times in incompatible ways.
`class` works as expected:
Animal = ml.class()
Animal.sound = '?'
function Animal:_init(name)
self.name = name
end
function Animal:speak()
return self._class.sound..' I am '..self.name
end
Cat = class(Animal)
Cat.sound = 'meow'
felix = Cat('felix')
assert(felix:speak() == 'meow I am felix')
assert(felix._base == Animal)
assert(Cat.classof(felix))
assert(Animal.classof(felix))
It creates a table (what else?) which will contain the methods; if there's a base class,
then that will be copied into the table. This table becomes the metatable of each new
instance of that class, with `__index` pointing to the metatable itself. If `obj.key` is
not found, then Lua will attempt to look it up in the class. In this way, each object
does not have to carry references to all of its methods, which gets inefficient.
The class is callable, and when called it returns a new object; if there is an `_init`
method that will be called to do any custom setup; if not then the base class constructor
will be called.
All classes have a `_class` field pointing to itself (which is how `Animal.speak` gets
its polymorphic behaviour) and a `classof` function.
## Array Class
Since Lua 5.1, the string functions can be called as methods, e.g. `s:sub(1,2)`. People
commonly would like this convenience for tables as well. But Lua tables are building
blocks; to build abstract data types you need to specialize your tables. So `ml` provides
a `Array` class:
local Array = ml.class()
-- A constructor can return a _specific_ object
function Array:_init(t)
if not t then return nil end -- no table, make a new one
if getmetatable(t)==Array then -- was already a Array: copy constructor!
t = ml.sub(t,1)
end
return t
end
function Array:map(f,...) return Array(ml.imap(f,self,...)) end
Note that if a constructor _does_ return a value, then it becomes the new object. This
flexibility is useful if you want to wrap _existing_ objects.
We can't just add `imap`, because the function signature is wrong; the first argument is
the function and it returns a plain jane array.
But we can add methods to the class directly if the functions have the right first
argument, and don't return anything:
ml.import(Array,{
-- straight from the table library
concat=table.concat,sort=table.sort,insert=table.insert,
remove=table.remove,append=table.insert,
...
})
`ifilter` and `sub` are almost right, but they need to be wrapped so that they return
Arrays as expected.
> words = Array{'frodo','bilbo','sam'}
> = words:sub(2)
{"bilbo","sam"}
> words:sort()
> = words
{"bilbo","frodo","sam"}
> = words:concat ','
"bilbo,frodo,sam"
> = words:filter(string.match,'o$'):map(string.upper)
{"BILBO","FRODO"}
Arrays are easier to use and involve less typing because the table functions are directly
available from them. Methods may be _chained_, which (I think) reads better than the
usual functional application order from right to left. For instance, the sort utility
discussed above simply becomes:
local Array = require 'ml'.Array
print(Array.collect(io.lines()):sort():concat '\n')
I don't generally recommend putting everything on one line, but it can be done if the
urge is strong ;)
The ml table functions are available as methods:
> l = Array.range(10,50,10)
> = l:indexof(30)
3
> = l:indexby {1,3,5}
{10,30,50}
> = l:map(function(x) return x + 1 end)
{11,21,31,41,51}
Lua anonymous functions have a somewhat heavy syntax; three keywords needed to define a
short lambda. It would be cool if the shorthand syntax `|x| x+1` used by Metalua would
make into mainstream Lua, but there seems to be widespread resistance to this little
convenience. In the meantime, there are _string lambdas_. All ml functions taking
function args go through `function_arg` which raises an error if the argument isn't
callable. But it will also understand 'X+1' as a shorthand for the above anonymous
function. Such strings are expressions containing the placeholder variables `X`,`Y` and
`Z` corresponding to the first, second and third arguments.
> A = Array
> a1 = A{1,2}
> a2 = A{10,20}
> = a1:map2('X+Y',a2)
{11,21}
String lambdas are more limited. There's no easy (or efficient) way for them to access
local variables like proper functions; they only see the global environment. BUt I
consider this a virtue, since they are intended to be 'pure' functions with no
side-effects.
Array is a useful class from which to derive more specialized classes, and it has
a very useful 'class method' to make adding new methods easy. In this case, we
intend to keep strings in this subclass, so it should have appropriate methods for
'bulk operations' using string methods.
Strings = class(Array)
Strings:mappers { -- NB: note the colon: class method
upper = string.upper,
match = string.match,
}
local s = Strings{'one','two','three'}
assert(s:upper() == Strings{'ONE','TWO','THREE'})
assert(s:match '.-e$' == Strings{'one','three'})
assert(s:sub(1,2):upper() == Strings{'ONE','TWO'})
In fact, Array has been designed to be extended. Note that the inherited method `sub`
is actually returning a Strings object, not a vanilla Array.
This property is usually known as _covariance_.
It's useful to remember that there is _nothing special_ about Array methods; they are
just functions which take an array-like table as the first argument. Saying
`Array.map(t,f)` where `t` is some random array-like table or object is fine -
but the result _will_ be an Array.
## Experiments!
Every library project has a few things which didn't make the final cut, and this is
particularly true of Microlight. The `ml_properties` module allows you to define
properties in your classes. This comes from `examples/test.lua':
local props = require 'ml_properties'
local P = class()
-- will be called after setting _props
function P:update (k,v)
last_set = k
end
-- any explicit setters will be called on construction
function P:set_name (name)
self.myname = name
end
function P:get_name ()
last_get = 'name'
return self.myname
end
-- have to call this after any setters or getters are defined...
props(P,{
__update = P.update;
enabled = true, -- these are default values
visible = false,
name = 'foo', -- has both a setter and a getter
})
local p = P()
-- initial state
asserteq (p,{myname="foo",_enabled=true,_visible=false})
p.visible = true
-- P.update fired!
asserteq(last_set,'visible')
`ml_range` (constributed by Dirk Laurie for this release) returns a function which works
like `ml.range`, except that it returns a Vector class which has element-wise addition
and multiplication operators.
`ml_module` is a Lua 5.2 module constructor which shows off that interesting function
`ml.import`. Here is the example in the distribution:
-- mod52.lua
local _ENV = require 'ml_module' (nil, -- no wholesale access to _G
'print','assert','os', -- quoted global values brought in
'lfs', -- not global, so use require()!
table -- not quoted, import the whole table into the environment!
)
function format (s)
local out = {'Hello',s,'at',os.date('%c'),'here is',lfs.currentdir()}
-- remember table.* has been brought in..
return concat(out,' ')
end
function message(s)
print(format(s))
end
-- no, we didn't bring anything else in
assert(setmetatable == nil)
-- NB return the _module_, not the _environment_!
return _M
This uses a 'shadow table' trick; the environment `_ENV` contains all the imports, plus
the exported functions; the actual module `_M` only contains the exported functions. So
it's equivalent to the old-fashioned `module('mod',package.seeall)` technique, except
that there is no way of accessing the environment of the module without using the debug
module - which you would never allow into a sandboxed environment anyway.
================================================
FILE: test
================================================
lua examples/test.lua
================================================
FILE: test-mod52.lua
================================================
local m = require 'mod52'
m.message 'you'
-- the module itself only contains the exported functions
-- (sandbox safe)
for k,v in pairs(m) do print(k,v) end
================================================
FILE: test.bat
================================================
lua examples\test.lua
gitextract_l7w06r43/ ├── compress.lua ├── config.ld ├── examples/ │ └── test.lua ├── microlight-1.1-1.rockspec ├── ml.lua ├── ml_module.lua ├── ml_properties.lua ├── ml_range.lua ├── mod52.lua ├── readme.md ├── test ├── test-mod52.lua └── test.bat
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (70K chars).
[
{
"path": "compress.lua",
"chars": 203,
"preview": "io.output 'compressed/ml.lua'\r\nfor line in io.lines 'ml.lua' do\r\n if not (line:match '^%s*$' or line:match '^%-%-') th"
},
{
"path": "config.ld",
"chars": 134,
"preview": "project='Microlight'\nfile='ml.lua'\ndescription='Compact Lua Utility Library'\nformat='markdown'\ntitle='Microlight'\nreadme"
},
{
"path": "examples/test.lua",
"chars": 7632,
"preview": "local ml = require 'ml'\nlocal A = ml.Array\nunpack = unpack or table.unpack\n\nml.import(_G,ml)\n\nlocal teq = Array.__eq\n\nfu"
},
{
"path": "microlight-1.1-1.rockspec",
"chars": 638,
"preview": "package = \"microlight\"\nversion = \"1.1-1\"\nsource = {\n url = \"git://github.com/stevedonovan/Microlight.git\",\n tag = \"1"
},
{
"path": "ml.lua",
"chars": 28193,
"preview": "-----------------\n-- Microlight - a very compact Lua utilities module\n--\n-- Steve Donovan, 2012; License MIT\n-- @module "
},
{
"path": "ml_module.lua",
"chars": 685,
"preview": "---------\n-- Simple Lua 5.2 module.\n-- Thin wrapper around ml.import\n--\n-- local _ENV = require 'ml_module' (_G)\n-- "
},
{
"path": "ml_properties.lua",
"chars": 2269,
"preview": "--- set properties table for an existing class.\n-- A property P can be fully specified by the class having\n-- 'get_P' an"
},
{
"path": "ml_range.lua",
"chars": 2770,
"preview": "-- ml_range.lua (c) 2012 Dirk Laurie, Lua-like MIT licence, except that\n-- Steve Donovan is allowed to use the code i"
},
{
"path": "mod52.lua",
"chars": 553,
"preview": "\nlocal _ENV = require 'ml_module' (nil, -- no wholesale access to _G\n 'print','assert','os', -- quoted global values "
},
{
"path": "readme.md",
"chars": 23544,
"preview": "# A Small but Useful Lua library\n\nThe Lua standard library is deliberately kept small, based on the abstract platform\nde"
},
{
"path": "test",
"chars": 22,
"preview": "lua examples/test.lua\n"
},
{
"path": "test-mod52.lua",
"chars": 158,
"preview": "local m = require 'mod52'\n\nm.message 'you'\n\n-- the module itself only contains the exported functions\n-- (sandbox safe)\n"
},
{
"path": "test.bat",
"chars": 23,
"preview": "lua examples\\test.lua\r\n"
}
]
About this extraction
This page contains the full source code of the stevedonovan/Microlight GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (65.3 KB), approximately 18.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.