{"id":15647389,"url":"https://github.com/stevedonovan/microlight","last_synced_at":"2025-08-21T17:31:36.098Z","repository":{"id":2495052,"uuid":"3469283","full_name":"stevedonovan/Microlight","owner":"stevedonovan","description":"A little library of useful Lua functions, intended as the 'light' version of Penlight","archived":false,"fork":false,"pushed_at":"2022-07-05T07:10:00.000Z","size":185,"stargazers_count":169,"open_issues_count":10,"forks_count":17,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-12-10T09:50:19.722Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://stevedonovan.github.com/microlight/","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevedonovan.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-02-17T11:12:44.000Z","updated_at":"2024-11-30T18:32:50.000Z","dependencies_parsed_at":"2022-07-08T19:39:08.746Z","dependency_job_id":null,"html_url":"https://github.com/stevedonovan/Microlight","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevedonovan%2FMicrolight","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevedonovan%2FMicrolight/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevedonovan%2FMicrolight/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevedonovan%2FMicrolight/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevedonovan","download_url":"https://codeload.github.com/stevedonovan/Microlight/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230523761,"owners_count":18239445,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-03T12:19:11.034Z","updated_at":"2024-12-20T02:06:19.110Z","avatar_url":"https://github.com/stevedonovan.png","language":"Lua","funding_links":[],"categories":["Resources"],"sub_categories":["Miscellaneous"],"readme":"# A Small but Useful Lua library\n\nThe Lua standard library is deliberately kept small, based on the abstract platform\ndefined by the C89 standard. It is intended as a base for further development, so Lua\nprogrammers tend to collect small useful functions for their projects.\n\nMicrolight is an attempt at 'library golf', by analogy to the popular nerd sport 'code\ngolf'. The idea here is to try capture some of these functions in one place and document\nthem well enough so that it is easier to use them than to write them yourself.\n\nThis library is intended to be a 'extra light' version of Penlight, which has nearly two\ndozen modules and hundreds of functions.\n\nIn Lua, anything beyond the core involves 'personal' choice, and this list of functions\ndoes not claim to aspire to 'canonical' status. It emerged from discussion on the Lua\nMailing list started by Jay Carlson, and was implemented by myself and Dirk Laurie.\n\n## Strings\n\nTHere is no built-in way to show a text representation of a Lua table, which can be\nfrustrating for people first using the interactive prompt. Microlight provides `tstring`.\nPlease note that globally redefining `tostring` is _not_ a good idea for Lua application\ndevelopment! This trick is intended to make experimation more satisfying:\n\n    \u003e require 'ml'.import()\n    \u003e tostring = tstring\n    \u003e = {10,20,name='joe'}\n    {10,20,name=\"joe\"}\n\nThe Lua string functions are particularly powerful but there are some common functions\nmissing that tend to come up in projects frequently. There is `table.concat` for building\na string out of a table, but no `table.split` to break a string into a table.\n\n    \u003e  = split('hello dolly')\n    {\"hello\",\"dolly\"}\n    \u003e = split('one,two',',')\n    {\"one\",\"two\"}\n\nThe second argument is a _string pattern_ that defaults to spaces.\n\nAlthough it's not difficult to do [string\ninterpolation](http://lua-users.org/wiki/StringInterpolation) in Lua, there's no little\nfunction to do it directly. So Microlight provides `ml.expand`.\n\n    \u003e = expand(\"hello $you, from $me\",{you='dolly',me='joe'})\n    hello dolly, from joe\n\n`expand` also understands the alternative `${var}` and may also be given a function, just\nlike `string.gsub`. (But pick one _or_ the other consistently.)\n\nLua string functions match using string patterns, which are a powerful subset of proper\nregular expressions: they contain 'magic' characters like '.','$' etc which you need to\nescape before using. `escape` is used when you wish to match a string literally:\n\n    \u003e = ('woo%'):gsub(escape('%'),'hoo')\n    \"woohoo\"   1\n    \u003e = split(\"1.2.3\",escape(\".\"))\n    {\"1\",\"2\",\"3\"}\n\n## Files and Paths\n\nAlthough `access` is available on most platforms, it's not part of the standard, (which\nis why it's spelt `_access` on Windows). So to test for the existance of a file, you need\nto attempt to open it. So the `exist` function is easy to write:\n\n    function ml.exists (filename)\n        local f = io.open(filename)\n        if not f then\n            return nil\n        else\n            f:close()\n            return filename\n        end\n    end\n\nThe return value is _not_ a simple true or false; it returns the filename if it exists so\nwe can easily find an existing file out of a group of candidates:\n\n    \u003e = exists 'README' or exists 'readme.txt' or exists 'readme.md'\n    \"readme.md\"\n\nLua is good at slicing and dicing text, so a common strategy is to read all of a\nnot-so-big file and process the string. This is the job of `readfile`. For instance, this\nreturns the first 128 bytes of the file opened in binary mode:\n\n    \u003e txt = readfile('readme.md',true):sub(1,128)\n\nNote I said bytes, not characters, since strings can contain any byte sequence.\n\nIf `readfile` can't open a file, or can't read from it, it will return `nil` and an error\nmessage. This is the pattern followed by `io.open` and many other Lua functions; it is\nconsidered bad form to raise an error for a _routine_ problem.\n\nBreaking up paths into their components is done with `splitpath` and `splitext`:\n\n    \u003e = splitpath(path)\n    \"/path/to/dogs\" \"bonzo.txt\"\n    \u003e = splitext(path)\n    \"/path/to/dogs/bonzo\"   \".txt\"\n    \u003e = splitpath 'frodo.txt'\n    \"\"      \"frodo.txt\"\n    \u003e = splitpath '/usr/'\n    \"/usr\"  \"\"\n    \u003e = splitext '/usr/bin/lua'\n    \"/usr/bin/lua\"  \"\"\n    \u003e\n\nThese functions return _two_ strings, one of which may be the empty string (rather than\n`nil`). On Windows, they use both forward- and back-slashes, on Unix only forward slashes.\n\n## Inserting and Extending\n\nMost of the Microlight functions work on Lua tables. Although these may be _both_ arrays\n_and_ hashmaps, generally we tend to _use_ them as one or the other. From now on, we'll\nuse array and map as shorthand terms for tables\n\n`update` adds key/value pairs to a map, and `extend` appends an array to an array; they\nare two complementary ways to add multiple items to a table in a single operation.\n\n    \u003e a = {one=1,two=2}\n    \u003e update(a,{three=3,four=4})\n    \u003e = a\n    {one=1,four=4,three=3,two=2}\n    \u003e t = {10,20,30}\n    \u003e extend(t,{40,50})\n    \u003e = t\n    {10,20,30,40,50}\n\nAs from version 1.1, both of these functions take an arbitrary number of tables.\n\nTo 'flatten' a table, just unpack it and use `extend`:\n\n    \u003e pair = {{1,2},{3,4}}\n    \u003e = extend({},unpack(pair))\n    {1,2,3,4}\n\n`extend({},t)` would just be a shallow copy of a table.\n\nMore precisely, `extend` takes an indexable and writeable object, where the index\nruns from 1 to `#O` with no holes, and starts adding new elements at `O[#O+1]`.\nSimularly, the other arguments are indexable but need not be writeable. These objects\nare typically tables, but don't need to be. You can exploit the guarantee that `extend`\nalways goes sequentially from 1 to `#T`, and make the first argument an object:\n\n    \u003e obj = setmetatable({},{ __newindex = function(t,k,v) print(v) end })\n    \u003e extend(obj,{1,2,3})\n    1\n    2\n    3\n\nTo insert multiple values into a position within an array, use `insertvalues`. It works\nlike `table.insert`, except that the third argument is an array of values. If you do want\nto overwrite values, then use `true` for the fourth argument:\n\n    \u003e t = {10,20,30,40,50}\n    \u003e insertvalues(t,2,{11,12})\n    \u003e = t\n    {10,11,12,20,30,40,50}\n    \u003e insertvalues(t,3,{2,3},true)\n    \u003e = t\n    {10,11,2,3,30,40,50}\n\n(Please note that the _original_ table is modified by these functions.)\n\n`update' works like `extend`. except that all the key value pairs from the input tables\nare copied into the first argument. Keys may be overwritten by subsequent tables.\n\n    \u003e t = {}\n    \u003e update(t,{one=1},{ein=1},{one='ONE'})\n    \u003e = t\n    {one=\"ONE\",ein=1}\n\n`import` is a specialized version of `update`; if the first argument is `nil` then it's\nassumed to be the global table. If no tables are provided, it brings in the ml table\nitself (hence the lazy `require \"ml\".import()` idiom).\n\nIf the arguments are strings, then we try to `require` them.  So this brings in\nLuaFileSystem and imports `lfs` into the global table. So it's a lazy way to do a whole\nbunch of requires. A module 'package.mod' will be brought in as `mod`. Note that the\nsecond form actually does bring all of `lpeg`'s functions in.\n\n    \u003e import(nil,'lfs')\n    \u003e import(nil,require 'lpeg')\n\n\n## Extracting and Mapping\n\nThe opposite operation to extending is extracting a number of items from a table.\n\nThere's `sub`, which works just like `string.sub` and is the equivalent of list slicing\nin Python:\n\n    \u003e numbers = {10,20,30,40,50}\n    \u003e = sub(numbers,1,1)\n    {10}\n    \u003e = sub(numbers,2)\n    {20,30,40,50}\n    \u003e = sub(numbers,1,-2)\n    {10,20,30,40}\n\n`indexby` indexes a table by an array of keys:\n\n    \u003e = indexby(numbers,{1,4})\n    {10,40}\n    \u003e = indexby({one=1,two=2,three=3},{'three','two'})\n    {[3,2}\n\nHere is the old standby `imap`, which makes a _new_ array by applying a function to the\noriginal elements:\n\n    \u003e words = {'one','two','three'}\n    \u003e = imap(string.upper,words)\n    {\"ONE\",\"TWO\",\"THREE\"}\n    \u003e s = {'10','x','20'}\n    \u003e ns = imap(tonumber,s)\n    \u003e = ns\n    {10,false,20}\n\n`imap` must always return an array of the same size - if the function returns `nil`, then\nwe avoid leaving a hole in the array by using `false` as a placeholder.\n\nAnother popular function `indexof` does a linear search for a value and returns the\n1-based index, or `nil` if not successful:\n\n    \u003e = indexof(numbers,20)\n    2\n    \u003e = indexof(numbers,234)\n    nil\n\nThis function takes an optional third argument, which is a custom equality function.\n\nIn general, you want to match something more than just equality. `ifind` will return the\nfirst value that satisfies the given function.\n\n    \u003e s = {'x','10','20','y'}\n    \u003e = ifind(s,tonumber)\n    \"10\"\n\nThe standard function `tonumber` returns a non-nil value, so the corresponding value is\nreturned - that is, the string. To get all the values that match, use `ifilter`:\n\n    \u003e = ifilter(numbers,tonumber)\n    {\"10\",\"20\"}\n\nThere is a useful hybrid between `imap` and `ifilter` called `imapfilter` which is\nparticularly suited to Lua use, where a function commonly returns either something useful,\nor nothing. (Phillip Janda originally suggested calling this `transmogrify`, since\nno-one has preconceptions about it, except that it's a cool toy for imaginative boys).\n\n    \u003e = imapfilter(tonumber,{'one',1,'f',23,2})\n    {1,23,2}\n\n`collect` makes a array out of an iterator. 'collectuntil` can be given a\ncustom predicate and `collectn` takes up to a maximum number of values,\nwhich is useful for iterators that never terminate.\n(Note that we need to pass it either a proper iterator, like `pairs`, or\na function or exactly one function - which isn't the case with `math.random`)\n\n    \u003e s = 'my dog ate your homework'\n    \u003e words = collect(s:gmatch '%a+')\n    \u003e = words\n    {\"my\",\"dog\",\"ate\",\"your\",\"homework\"}\n    \u003e R = function() return math.random() end\n    \u003e = collectn(3,R)\n    {0.0012512588885159,0.56358531449324,0.19330423902097}\n    \u003e lines = collectuntil(4,io.lines())\n    one\n    two\n    three\n    four\n    \u003e = lines\n    {\"one\",\"two\",\"three\",\"four\"}\n\nA simple utility to sort standard input looks like this:\n\n    require 'ml'.import()\n    lines = collect(io.lines())\n    table.sort(lines)\n    print(table.concat(lines,'\\n'))\n\nAnother standard function that can be used here is `string.gmatch`.\n\nLuaFileSystem defines an iterator over directory contents. `collect(lfs.dir(D))` gives\nyou an _array_ of all files in directory `D`.\n\nFinally, `removerange` removes a _range_ of values from an array, and takes the same\narguments as `sub`. Unlike the filters filters, it works in-place.\n\n## Sets and Maps\n\n`indexof` is not going to be your tool of choice for really big tables, since it does a\nlinear search. Lookup on Lua hash tables is faster, if we can get the data into the right\nshape.  `invert` turns a array of values into a table with those values as keys:\n\n    \u003e m = invert(numbers)\n    \u003e = m\n    {[20]=2,[10]=1,[40]=4,[30]=3,[50]=5}\n    \u003e = m[20]\n    2\n    \u003e = m[30]\n    3\n    \u003e = m[25]\n    nil\n    \u003e m = invert(words)\n    \u003e = m\n    {one=1,three=3,two=2}\n\nSo from a array we get a reverse lookup map. This is also exactly what we want from a\n_set_: fast membership test and unique values.\n\nSets don't particularly care about the actual value, as long as it evaluates as true or\nfalse, hence:\n\n    \u003e = issubset(m,{one=true,two=true})\n    true\n\n `makemap` takes another argument and makes up a table where the keys come from the first\narray and the values from the second array:\n\n    \u003e = makemap({'a','b','c'},{1,2,3})\n    {a=1,c=3,b=2}\n\n\n# Higher-order Functions\n\nFunctions are first-class values in Lua, so functions may manipulate them, often called\n'higher-order' functions.\n\nBy _callable_ we either mean a function or an object which has a `__call` metamethod. The\n`callable` function checks for this case.\n\nFunction _composition_ is often useful:\n\n    \u003e printf = compose(io.write,string.format)\n    \u003e printf(\"the answer is %d\\n\",42)\n    the answer is 42\n\n`bind1` and `bind2` specialize functions by creating a version that has one less\nargument. `bind1` gives a function where the first argument is _bound_ to some value.\nThis can be used to pass methods to functions expecting a plain function. In Lua,\n`obj:f()` is shorthand for `obj.f(obj,...)`. Just using a dot is not enough, since there\nis no _implicit binding_ of the self argument. This is precisely what `bind1` can do:\n\n    \u003e ewrite = bind1(io.stderr.write,io.stderr)\n    \u003e ewrite 'hello\\n'\n\nWe want a logging function that writes a message to standard error with a line feed; just\nbind the second argument to '\\n':\n\n    \u003e log = bind2(ewrite,'\\n')\n    \u003e log 'hello'\n    hello\n\nNote that `sub(t,1)` does a simple array copy:\n\n    \u003e copy = bind2(sub,1)\n    \u003e t = {1,2,3}\n    \u003e = copy(t)\n    {1,2,3}\n\nIt's easy to make a 'predicate' for detecting empty or blank strings:\n\n    \u003e blank = bind2(string.match,'^%s*$')\n    \u003e = blank ''\n    \"\"\n    \u003e = blank '  '\n    \"  \"\n    \u003e = blank 'oy vey'\n    nil\n\nI put 'predicate' in quotes because it's again not classic true/false; Lua actually only\ndeveloped `false` fairly late in its career. Operationally, this is a fine predicate\nbecause `nil` matches as 'false' and any string matches as 'true'.\n\nThis pattern generates a whole family of classification functions, e.g. `hex` (using\n'%x+'), `upcase` ('%u+'), `iden` ('%a[%w_]*') and so forth. You can keep the binding game\ngoing (after all, `bind2` is just a function like any other.)\n\n    \u003e matcher = bind1(bind2,string.match)\n    \u003e hex = matcher '^%x+$'\n\nPredicates are particularly useful for `ifind` and `ifilter`.  It's now easy to filter\nout strings from a array that match `blank` or `hex`, for instance.\n\nIt is not uncommon for Lua functions to return multiple useful values; sometimes the one\nyou want is the second value - this is what `take2` does:\n\n    \u003e p = lfs.currentdir()\n    \u003e = p\n    \"C:\\\\Users\\\\steve\\\\lua\\\\Microlight\"\n    \u003e = splitpath(p)\n    \"C:\\\\Users\\\\steve\\\\lua\" \"Microlight\"\n    \u003e basename = take2(splitpath)\n    \u003e = basename(p)\n    \"Microlight\"\n    \u003e extension = take2(splitext)\n    \u003e = extension 'bonzo.dog'\n    \".dog\"\n\nThere is a pair of functions `map2fun` and `fun2map` which convert indexable objects into\ncallables and vice versa. Say I have a table of key/value pairs, but an API requires a\nfunction - use `map2fun`. Alternatively, the API might want a lookup table and you only\nhave a lookup function.  Say we have an array of objects with a name field. The `find`\nmethod will give us an object with a particular name:\n\n    \u003e obj = objects:find ('X.name=Y','Alfred')\n    {name='Afred',age=23}\n    \u003e by_name = function(name) return objects:find('X.name=Y',name) end\n    \u003e lookup = fun2map(by_name)\n    \u003e = lookup.Alfred\n    {name='Alfred',age=23}\n\nNow if you felt particularly clever and/or sadistic, that anonymous function could be\nwritten like so: (note the different quotes needed to get a nested string lambda):\n\n    by_name = bind1('X:find(\"X.name==Y\",Y)',objects)\n\n## Classes\n\nLua and Javascript have two important things in common; objects are associative arrays,\nwith sugar so that `t.key == t['key']`; there is no built-in class mechanism. This causes\na lot of (iniital) unhappiness. It's straightforward to build a class system, and so it\nis reinvented numerous times in incompatible ways.\n\n`class` works as expected:\n\n    Animal = ml.class()\n    Animal.sound = '?'\n\n    function Animal:_init(name)\n        self.name = name\n    end\n\n    function Animal:speak()\n        return self._class.sound..' I am '..self.name\n    end\n\n    Cat = class(Animal)\n    Cat.sound = 'meow'\n\n    felix = Cat('felix')\n\n    assert(felix:speak() == 'meow I am felix')\n    assert(felix._base == Animal)\n    assert(Cat.classof(felix))\n    assert(Animal.classof(felix))\n\n\nIt creates a table (what else?) which will contain the methods; if there's a base class,\nthen that will be copied into the table. This table becomes the metatable of each new\ninstance of that class, with `__index` pointing to the metatable itself. If `obj.key` is\nnot found, then Lua will attempt to look it up in the class. In this way, each object\ndoes not have to carry references to all of its methods, which gets inefficient.\n\nThe class is callable, and when called it returns a new object; if there is an `_init`\nmethod that will be called to do any custom setup; if not then the base class constructor\nwill be called.\n\nAll classes have a `_class` field pointing to itself (which is how `Animal.speak` gets\nits polymorphic behaviour) and a `classof` function.\n\n## Array Class\n\nSince Lua 5.1, the string functions can be called as methods, e.g. `s:sub(1,2)`. People\ncommonly would like this convenience for tables as well. But Lua tables are building\nblocks; to build abstract data types you need to specialize your tables. So `ml` provides\na `Array` class:\n\n    local Array = ml.class()\n\n    -- A constructor can return a _specific_ object\n    function Array:_init(t)\n        if not t then return nil end  -- no table, make a new one\n        if getmetatable(t)==Array then  -- was already a Array: copy constructor!\n            t = ml.sub(t,1)\n        end\n        return t\n    end\n\n    function Array:map(f,...) return Array(ml.imap(f,self,...)) end\n\nNote that if a constructor _does_ return a value, then it becomes the new object. This\nflexibility is useful if you want to wrap _existing_ objects.\n\nWe can't just add `imap`, because the function signature is wrong; the first argument is\nthe function and it returns a plain jane array.\n\nBut we can add methods to the class directly if the functions have the right first\nargument, and don't return anything:\n\n    ml.import(Array,{\n        -- straight from the table library\n        concat=table.concat,sort=table.sort,insert=table.insert,\n        remove=table.remove,append=table.insert,\n    ...\n    })\n\n`ifilter` and `sub` are almost right, but they need to be wrapped so that they return\nArrays as expected.\n\n    \u003e words = Array{'frodo','bilbo','sam'}\n    \u003e = words:sub(2)\n    {\"bilbo\",\"sam\"}\n    \u003e words:sort()\n    \u003e = words\n    {\"bilbo\",\"frodo\",\"sam\"}\n    \u003e = words:concat ','\n    \"bilbo,frodo,sam\"\n    \u003e = words:filter(string.match,'o$'):map(string.upper)\n    {\"BILBO\",\"FRODO\"}\n\nArrays are easier to use and involve less typing because the table functions are directly\navailable from them.  Methods may be _chained_, which (I think) reads better than the\nusual functional application order from right to left.  For instance, the sort utility\ndiscussed above simply becomes:\n\n    local Array = require 'ml'.Array\n    print(Array.collect(io.lines()):sort():concat '\\n')\n\nI don't generally recommend putting everything on one line, but it can be done if the\nurge is strong ;)\n\nThe ml table functions are available as methods:\n\n    \u003e l = Array.range(10,50,10)\n    \u003e = l:indexof(30)\n    3\n    \u003e = l:indexby {1,3,5}\n    {10,30,50}\n    \u003e = l:map(function(x) return x + 1 end)\n    {11,21,31,41,51}\n\nLua anonymous functions have a somewhat heavy syntax; three keywords needed to define a\nshort lambda.  It would be cool if the shorthand syntax `|x| x+1` used by Metalua would\nmake into mainstream Lua, but there seems to be widespread resistance to this little\nconvenience. In the meantime, there are _string lambdas_.  All ml functions taking\nfunction args go through `function_arg` which raises an error if the argument isn't\ncallable. But it will also understand 'X+1' as a shorthand for the above anonymous\nfunction. Such strings are expressions containing the placeholder variables `X`,`Y` and\n`Z` corresponding to the first, second and third arguments.\n\n    \u003e A = Array\n    \u003e a1 = A{1,2}\n    \u003e a2 = A{10,20}\n    \u003e = a1:map2('X+Y',a2)\n    {11,21}\n\nString lambdas are more limited. There's no easy (or efficient) way for them to access\nlocal variables like proper functions; they only see the global environment. BUt I\nconsider this a virtue, since they are intended to be 'pure' functions with no\nside-effects.\n\nArray is a useful class from which to derive more specialized classes, and it has\na very useful 'class method' to make adding new methods easy. In this case, we\nintend to keep strings in this subclass, so it should have appropriate methods for\n'bulk operations' using string methods.\n\n    Strings = class(Array)\n\n    Strings:mappers {  -- NB: note the colon: class method\n        upper = string.upper,\n        match = string.match,\n    }\n\n    local s = Strings{'one','two','three'}\n\n    assert(s:upper() == Strings{'ONE','TWO','THREE'})\n    assert(s:match '.-e$' == Strings{'one','three'})\n    assert(s:sub(1,2):upper() == Strings{'ONE','TWO'})\n\nIn fact, Array has been designed to be extended. Note that the inherited method `sub`\nis actually returning a Strings object, not a vanilla Array.\nThis property is usually known as _covariance_.\n\nIt's useful to remember that there is _nothing special_ about Array methods; they are\njust functions which take an array-like table as the first argument.  Saying\n`Array.map(t,f)` where `t` is some random array-like table or object is fine -\nbut the result _will_ be an Array.\n\n\n## Experiments!\n\nEvery library project has a few things which didn't make the final cut, and this is\nparticularly true of Microlight.  The `ml_properties` module allows you to define\nproperties in your classes. This comes from `examples/test.lua':\n\n    local props = require 'ml_properties'\n\n    local P = class()\n\n    -- will be called after setting _props\n    function P:update (k,v)\n        last_set = k\n    end\n\n    -- any explicit setters will be called on construction\n    function P:set_name (name)\n        self.myname = name\n    end\n\n    function P:get_name ()\n        last_get = 'name'\n        return self.myname\n    end\n\n    -- have to call this after any setters or getters are defined...\n    props(P,{\n        __update = P.update;\n        enabled = true,  -- these are default values\n        visible = false,\n        name = 'foo', -- has both a setter and a getter\n    })\n\n    local p = P()\n\n    -- initial state\n    asserteq (p,{myname=\"foo\",_enabled=true,_visible=false})\n\n    p.visible = true\n\n    -- P.update fired!\n    asserteq(last_set,'visible')\n\n`ml_range` (constributed by Dirk Laurie for this release) returns a function which works\nlike `ml.range`, except that it returns a Vector class which has element-wise addition\nand multiplication operators.\n\n`ml_module` is a Lua 5.2 module constructor which shows off that interesting function\n`ml.import`.  Here is the example in the distribution:\n\n    -- mod52.lua\n    local _ENV = require 'ml_module' (nil, -- no wholesale access to _G\n        'print','assert','os', -- quoted global values brought in\n        'lfs', -- not global, so use require()!\n        table -- not quoted, import the whole table into the environment!\n        )\n\n    function format (s)\n        local out = {'Hello',s,'at',os.date('%c'),'here is',lfs.currentdir()}\n        -- remember table.* has been brought in..\n        return concat(out,' ')\n    end\n\n    function message(s)\n        print(format(s))\n    end\n\n    -- no, we didn't bring anything else in\n    assert(setmetatable == nil)\n\n    -- NB return the _module_, not the _environment_!\n    return _M\n\nThis uses a 'shadow table' trick; the environment `_ENV` contains all the imports, plus\nthe exported functions; the actual module `_M` only contains the exported functions.  So\nit's equivalent to the old-fashioned `module('mod',package.seeall)` technique, except\nthat there is no way of accessing the environment of the module without using the debug\nmodule - which you would never allow into a sandboxed environment anyway.\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevedonovan%2Fmicrolight","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevedonovan%2Fmicrolight","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevedonovan%2Fmicrolight/lists"}