├── .github └── workflows │ └── spec.yml ├── .gitignore ├── .luacov ├── AUTHORS.md ├── LICENSE.md ├── Makefile ├── NEWS.md ├── README.md ├── build-aux └── config.ld.in ├── lib └── std │ └── functional │ ├── _base.lua │ ├── init.lua │ ├── operator.lua │ └── tuple.lua ├── spec ├── functional_spec.yaml ├── operator_spec.yaml ├── spec_helper.lua └── tuple_spec.yaml └── std.functional-git-1.rockspec /.github/workflows/spec.yml: -------------------------------------------------------------------------------- 1 | name: spec 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ 'master' ] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | lua-version: ["5.4", "5.3", "5.2", "5.1", "luajit"] 15 | typecheck: ["typecheck", ""] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - uses: leafo/gh-actions-lua@v8.0.0 23 | with: 24 | luaVersion: ${{ matrix.lua-version }} 25 | 26 | - uses: leafo/gh-actions-luarocks@v4.0.0 27 | 28 | - name: install 29 | run: | 30 | sudo apt-get install -y libyaml-dev 31 | luarocks install ansicolors 32 | luarocks install ldoc 33 | luarocks install luacov 34 | luarocks install specl 35 | test -n "${{ matrix.typecheck }}" && luarocks install typecheck || true 36 | 37 | - name: build 38 | run: | 39 | make all doc 40 | luarocks make 41 | 42 | - name: test 43 | run: | 44 | make check SPECL_OPTS='-vfreport --coverage' 45 | bash <(curl -s https://codecov.io/bash) -f luacov.report.out 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /build-aux/config.ld 3 | /doc/* 4 | /functional-*.src.rock 5 | /lib/std/functional/version.lua 6 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | return { 2 | -- filename to store stats collected 3 | ["statsfile"] = "luacov.stats.out", 4 | 5 | -- filename to store report 6 | ["reportfile"] = "luacov.report.out", 7 | 8 | -- luacov.stats file updating frequency. 9 | -- The lower this value - the more frequenty results will be written out to luacov.stats 10 | -- You may want to reduce this value for short lived scripts (to for example 2) to avoid losing coverage data. 11 | ["savestepsize"] = 100, 12 | 13 | -- Run reporter on completion? (won't work for ticks) 14 | runreport = true, 15 | 16 | -- Delete stats file after reporting? 17 | deletestats = false, 18 | 19 | -- Process Lua code loaded from raw strings 20 | -- (that is, when the 'source' field in the debug info 21 | -- does not start with '@') 22 | codefromstrings = false, 23 | 24 | -- Patterns for files to include when reporting 25 | -- all will be included if nothing is listed 26 | -- (exclude overrules include, do not include 27 | -- the .lua extension, path separator is always '/') 28 | ["include"] = { 29 | "lib/std/functional/_base$", 30 | "lib/std/functional/init$", 31 | "lib/std/functional/operator$", 32 | "lib/std/functional/tuple$", 33 | "lib/std/functional/version$", 34 | }, 35 | 36 | -- Patterns for files to exclude when reporting 37 | -- all will be included if nothing is listed 38 | -- (exclude overrules include, do not include 39 | -- the .lua extension, path separator is always '/') 40 | ["exclude"] = { 41 | "luacov$", 42 | "luacov/reporter$", 43 | "luacov/defaults$", 44 | "luacov/runner$", 45 | "luacov/stats$", 46 | "luacov/tick$", 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # functional's contributors 2 | 3 | This file lists major contributors to _functional_. If you think you 4 | should be listed, please raise a [github][] issue. Thanks also to all 5 | those who have contributed bug fixes, suggestions and support. 6 | 7 | Gary V. Vaughan now maintains _functional_, having rewritten and 8 | reorganized all the original code from [lua-stdlib][], in addition to 9 | adding a lot of new functionality. 10 | 11 | Reuben Thomas started the standard libraries project, which included the 12 | original implementation of many of the functions now distributed with 13 | this package. 14 | 15 | [github]: https://github.com/lua-stdlib/functional/issues 16 | [lua-stdlib]: https://github.com/lua-stdlib/lua-stdlib 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2002-2022 std.functional authors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGE- 17 | MENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 18 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Functional API for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2002-2022 std.functional authors 3 | 4 | LDOC = ldoc 5 | LUA = lua 6 | MKDIR = mkdir -p 7 | SED = sed 8 | SPECL = specl 9 | 10 | VERSION = git 11 | 12 | luadir = lib/std/functional 13 | SOURCES = \ 14 | $(luadir)/_base.lua \ 15 | $(luadir)/init.lua \ 16 | $(luadir)/operator.lua \ 17 | $(luadir)/tuple.lua \ 18 | $(luadir)/version.lua \ 19 | $(NOTHING_ELSE) 20 | 21 | 22 | all: $(luadir)/version.lua 23 | 24 | 25 | $(luadir)/version.lua: Makefile 26 | echo 'return "Functional Programming Libraries / $(VERSION)"' > '$@' 27 | 28 | doc: build-aux/config.ld $(SOURCES) 29 | $(LDOC) -c build-aux/config.ld . 30 | 31 | build-aux/config.ld: build-aux/config.ld.in 32 | $(SED) -e "s,@PACKAGE_VERSION@,$(VERSION)," '$<' > '$@' 33 | 34 | 35 | CHECK_ENV = LUA=$(LUA) 36 | 37 | check: 38 | LUA=$(LUA) $(SPECL) $(SPECL_OPTS) spec/*_spec.yaml 39 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # std.functional NEWS - User visible changes 2 | 3 | ## Noteworthy changes in release ?.? (????-??-?) [?] 4 | 5 | 6 | ## Noteworthy changes in release 1.0 (2022-01-04) [stable] 7 | 8 | ### New features (since lua-stdlib-41.2) 9 | 10 | - Initial release, now separated out from lua-stdlib. 11 | 12 | - `memoize` uses a fast stable render based serialization call by 13 | default now, when the _mnemonic_ parameter is not given. 14 | 15 | - New `operator.eqv` is similar to `operator.eq`, except that it 16 | succeeds when recursive table contents are equivalent. 17 | 18 | - `operator.eqv` also uses render to determine equivalence between 19 | tables, which means it works correctly for table key equivalence too. 20 | 21 | - Passing the result of `lambda` to `tostring` returns the original 22 | lambda string. 23 | 24 | - New `tuple` object, for managing interned immutable nil-preserving 25 | tuples: 26 | 27 | ```lua 28 | local Tuple = require "std.functional.tuple" 29 | local t3 = Tuple (nil, false, nil) 30 | local t3_ = Tuple (nil, false, nil) 31 | assert (t3 == t3_) 32 | 33 | local len = require "std.functional.operator".len 34 | assert (len (t3) == 3) 35 | 36 | local t = {} 37 | for i = 1, len (t3) do t = t3[i] end 38 | assert (len (t) == 3) 39 | 40 | local a, b, c = t3 () 41 | assert (a == nil and b == false and c == nil) 42 | ``` 43 | 44 | - New `any` returns a function that calls each of the passed functions 45 | with the same arguments, stopping and returning the result from the 46 | first of those calls that does not equal `nil`. 47 | 48 | - New `product` returns a list of combinations made by taking one element 49 | from each of the argument lists. See LDocs for an example. 50 | 51 | - New `operator.len` improves upon `std.table.len`. 52 | `operator.len` is always deterministic; counting only 53 | numerically indexed elements immediately up to the first `nil` valued 54 | element (PUC-Rio Lua does not guarantee this with its `#` operator): 55 | 56 | ```lua 57 | local t1 = {1, 2, [5]=3} 58 | local t2 = {1, 2, nil, nil, 3} 59 | assert (eqv (t1, t2)) 60 | assert (len (t1) == len (t2)) 61 | assert (#t1 == #t2) --> PUC-Rio Lua: might be false! 62 | ``` 63 | 64 | - `ireverse` improves upon `std.ireverse` implementing the functional 65 | style of a non-destructive sequence reversing operation. 66 | 67 | - `flatten` and `shape` improve upon `std.table.flatten` and 68 | `std.table.shape` by being far more useful for functional programming 69 | style than using regular tables in imperative code. 70 | 71 | 72 | ### Bug fixes 73 | 74 | - `any`, `bind` and `compose` return functions that propagate trailing 75 | `nil` arguments correctly. 76 | 77 | - `memoize` now considers trailing nil arguments when looking up a 78 | memoized value for those particular arguments, and propagates `nil` 79 | return values from _mnemonic_ functions correctly. 80 | 81 | - `filter`, `map` and `reduce` now pass trailing nil arguments to their 82 | iterator function correctly. 83 | 84 | 85 | ### Incompatible changes 86 | 87 | - `lambda` no longer returns a bare function, but a functable that can be 88 | called and stringified. 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Functional Programming with Lua 2 | =============================== 3 | 4 | Copyright (C) 2002-2022 [std.functional authors][authors] 5 | 6 | [![License](http://img.shields.io/:license-mit-blue.svg)](https://mit-license.org/2002) 7 | [![workflow status](https://github.com/lua-stdlib/functional/actions/workflows/spec.yml/badge.svg?branch=master)](https://github.com/lua-stdlib/functional/actions) 8 | [![codecov.io](https://codecov.io/gh/lua-stdlib/functional/branch/master/graph/badge.svg)](https://codecov.io/gh/lua-stdlib/functional) 9 | 10 | 11 | This is a collection of Functional Programming libraries for Lua 5.1 12 | (including LuaJIT), 5.2, 5.3 and 5.4. The libraries are copyright by their 13 | authors (see the [AUTHORS][] file for details), and released under the 14 | [MIT license][mit] (the same license as Lua itself). There is no warranty. 15 | 16 | _std.functional_ has no run-time prerequisites beyond a standard Lua system, 17 | though it will take advantage of [stdlib][], [strict][] and [typecheck][] 18 | if they are installed. 19 | 20 | [authors]: https://github.com/lua-stdlib/functional/blob/master/AUTHORS.md 21 | [github]: https://github.com/lua-stdlib/functional/ "Github repository" 22 | [lua]: https://www.lua.org "The Lua Project" 23 | [mit]: https://mit-license.org "MIT License" 24 | [stdlib]: https://github.com/lua-stdlib/lua-stdlib "Standard Lua Libraries" 25 | [strict]: https://github.com/lua-stdlib/strict "strict variables" 26 | [typecheck]: https://github.com/gvvaughan/typecheck "function type checks" 27 | 28 | 29 | Installation 30 | ------------ 31 | 32 | The simplest and best way to install std.functional is with [LuaRocks][]. To 33 | install the latest release): 34 | 35 | ```bash 36 | luarocks install std.functional 37 | ``` 38 | 39 | To install current git master (for testing, before submitting a bug 40 | report for example): 41 | 42 | ```bash 43 | luarocks install http://github.com/lua-stdlib/functional/raw/master/std.functional-git-1.rockspec 44 | ``` 45 | 46 | The best way to install without [LuaRocks][] is to copy the `std/functional` 47 | folder and its contents into a directory on your package search path. 48 | 49 | [luarocks]: https://www.luarocks.org "Lua package manager" 50 | 51 | 52 | Documentation 53 | ------------- 54 | 55 | The latest release of these libraries is [documented in LDoc][github.io]. 56 | Pre-built HTML files are included in the release. 57 | 58 | [github.io]: https://lua-stdlib.github.io/functional 59 | 60 | 61 | Bug reports and code contributions 62 | ---------------------------------- 63 | 64 | These libraries are written and maintained by their users. 65 | 66 | Please make bug reports and suggestions as [GitHub Issues][issues]. 67 | Pull requests are especially appreciated. 68 | 69 | But first, please check that your issue has not already been reported by 70 | someone else, and that it is not already fixed by [master][github] in 71 | preparation for the next release (see Installation section above for how 72 | to temporarily install master with [LuaRocks][]). 73 | 74 | There is no strict coding style, but please bear in mind the following 75 | points when proposing changes: 76 | 77 | 0. Follow existing code. There are a lot of useful patterns and avoided 78 | traps there. 79 | 80 | 1. 3-character indentation using SPACES in Lua sources: It makes rogue 81 | TABs easier to see, and lines up nicely with 'if' and 'end' keywords. 82 | 83 | 2. Simple strings are easiest to type using single-quote delimiters, 84 | saving double-quotes for where a string contains apostrophes. 85 | 86 | 3. Save horizontal space by only using SPACEs where the parser requires 87 | them. 88 | 89 | 4. Use vertical space to separate out compound statements to help the 90 | coverage reports discover untested lines. 91 | 92 | [issues]: https://github.com/lua-stdlib/functional/issues 93 | -------------------------------------------------------------------------------- /build-aux/config.ld.in: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional API for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2002-2022 std.functional authors 4 | ]] 5 | 6 | title = 'functional @PACKAGE_VERSION@ Reference' 7 | project = 'functional @PACKAGE_VERSION@' 8 | 9 | description = [[ 10 | # Functional Programming with Lua 11 | 12 | An immutable tuple object, and many higher-order functions to help 13 | program in a functional style using tuples and Lua tables. 14 | 15 | This is a light-weight library for Lua 5.1 (including LuaJIT), 5.2 16 | and 5.3 written in pure Lua. 17 | 18 | ## LICENSE 19 | 20 | The code is copyright by its authors, and released under the MIT 21 | license (the same license as Lua itself). There is no warranty. 22 | ]] 23 | 24 | dir = '../doc' 25 | 26 | file = { 27 | '../lib/std/functional/init.lua', 28 | '../lib/std/functional/operator.lua', 29 | '../lib/std/functional/tuple.lua', 30 | } 31 | 32 | new_type ('object', 'Objects', false, 'Fields') 33 | 34 | format = 'markdown' 35 | backtick_references = false 36 | sort = false 37 | -------------------------------------------------------------------------------- /lib/std/functional/_base.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2002-2022 std.functional authors 4 | ]] 5 | --[[-- 6 | Purely to break internal dependency cycles without introducing 7 | multiple copies of base functions used in other modules. 8 | 9 | @module std.functional._base 10 | ]] 11 | 12 | local _ENV = require 'std.normalize' { 13 | 'string.format', 14 | 'table.concat', 15 | 'table.keys', 16 | 'table.sort', 17 | } 18 | 19 | 20 | local argscheck 21 | do 22 | local ok, typecheck = pcall(require, 'typecheck') 23 | if ok then 24 | argscheck = typecheck.argscheck 25 | else 26 | argscheck = function(decl, fn) 27 | return fn 28 | end 29 | end 30 | end 31 | 32 | 33 | 34 | --[[ ================= ]]-- 35 | --[[ Shared Functions. ]]-- 36 | --[[ ================= ]]-- 37 | 38 | 39 | local function shallow_copy(t) 40 | local r = {} 41 | for k, v in next, t do 42 | r[k] = v 43 | end 44 | return r 45 | end 46 | 47 | 48 | local function toqstring(x) 49 | if type(x) ~= 'string' then 50 | return tostring(x) 51 | end 52 | return format('%q', x) 53 | end 54 | 55 | 56 | local function keycmp(a, b) 57 | if type(a) == 'number' then 58 | return type(b) ~= 'number' or a < b 59 | else 60 | return type(b) ~= 'number' and tostring(a) < tostring(b) 61 | end 62 | end 63 | 64 | 65 | local function sortedkeys(t, cmp) 66 | local r = keys(t) 67 | sort(r, cmp) 68 | return r 69 | end 70 | 71 | 72 | local function serialize(x, roots) 73 | roots = roots or {} 74 | 75 | local function stop_roots(x) 76 | return roots[x] or serialize(x, shallow_copy(roots)) 77 | end 78 | 79 | if type(x) ~= 'table' or getmetamethod(x, '__tostring') then 80 | return toqstring(x) 81 | 82 | else 83 | local buf = {'{'} -- pre-buffer table open 84 | roots[x] = toqstring(x) -- recursion protection 85 | 86 | local kp -- previous key 87 | for _, k in ipairs(sortedkeys(x, keycmp)) do 88 | if kp ~= nil then 89 | buf[#buf + 1] = ',' 90 | end 91 | buf[#buf + 1] = stop_roots(kp) .. '=' .. stop_roots(x[k]) 92 | kp = k 93 | end 94 | buf[#buf + 1] = '}' -- buffer << table close 95 | 96 | return concat(buf) -- stringify buffer 97 | end 98 | end 99 | 100 | 101 | 102 | --[[ ================= ]]-- 103 | --[[ Public Interface. ]]-- 104 | --[[ ================= ]]-- 105 | 106 | 107 | return { 108 | argscheck = argscheck, 109 | serialize = serialize, 110 | toqstring = toqstring, 111 | } 112 | -------------------------------------------------------------------------------- /lib/std/functional/init.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2002-2022 std.functional authors 4 | ]] 5 | --[[-- 6 | Functional programming. 7 | 8 | A selection of higher-order functions to enable a functional style of 9 | programming in Lua. 10 | 11 | @module std.functional 12 | ]] 13 | 14 | 15 | local _ENV = require 'std.normalize' { 16 | 'coroutine.wrap', 17 | 'coroutine.yield', 18 | 'math.ceil', 19 | 'std.functional._base.argscheck', 20 | 'std.functional._base.serialize', 21 | 'table.concat', 22 | 'table.remove', 23 | } 24 | 25 | 26 | 27 | -- FIXME: Figure these two out: 28 | 29 | local function ielems(t) 30 | -- capture ipairs iterator initial state 31 | local fn, istate, ctrl = ipairs(t) 32 | return function(state, _) 33 | local v 34 | ctrl, v = fn(state, ctrl) 35 | if ctrl then 36 | return v 37 | end 38 | end, istate, true -- wrapped initial state 39 | end 40 | 41 | 42 | local function leaves(it, tr) 43 | local function visit(n) 44 | if type(n) == 'table' then 45 | for _, v in it(n) do 46 | visit(v) 47 | end 48 | else 49 | yield(n) 50 | end 51 | end 52 | return wrap(visit), tr 53 | end 54 | 55 | 56 | 57 | --[[ =============== ]]-- 58 | --[[ Implementation. ]]-- 59 | --[[ =============== ]]-- 60 | 61 | 62 | local function any(...) 63 | local fns = pack(...) 64 | 65 | return function(...) 66 | local argt = {n = 0} 67 | for i = 1, fns.n do 68 | argt = pack(fns[i](...)) 69 | if argt[1] ~= nil then 70 | return unpack(argt, 1, argt.n) 71 | end 72 | end 73 | return unpack(argt, 1, argt.n) 74 | end 75 | end 76 | 77 | 78 | local ceil = ceil -- FIXME: specl-14.x breaks function environments here :( 79 | 80 | local function bind(fn, bound) 81 | return function(...) 82 | local argt, unbound = {}, pack(...) 83 | 84 | -- Inline `argt = copy(bound)`... 85 | local n = bound.n or 0 86 | for k, v in next, bound do 87 | -- ...but only copy integer keys. 88 | if type(k) == 'number' and ceil(k) == k then 89 | argt[k] = v 90 | n = k > n and k or n -- Inline `n = maxn(unbound)` in same pass. 91 | end 92 | end 93 | 94 | -- Bind *unbound* parameters sequentially into *argt* gaps. 95 | local i = 1 96 | for j = 1, unbound.n do 97 | while argt[i] ~= nil do 98 | i = i + 1 99 | end 100 | argt[i], i = unbound[j], i + 1 101 | end 102 | 103 | -- Even if there are gaps remaining above *i*, pass at least *n* args. 104 | if n >= i then 105 | return fn(unpack(argt, 1, n)) 106 | end 107 | 108 | -- Otherwise, we filled gaps beyond *n*, and pass that many args. 109 | return fn(unpack(argt, 1, i - 1)) 110 | end 111 | end 112 | 113 | 114 | -- No need to recurse because functables are second class citizens in 115 | -- Lua: 116 | -- func=function() print 'called' end 117 | -- func() --> 'called' 118 | -- functable=setmetatable({}, {__call=func}) 119 | -- functable() --> 'called' 120 | -- nested=setmetatable({}, {__call=functable}) 121 | -- nested() 122 | -- --> stdin:1: attempt to call a table value(global 'd') 123 | -- --> stack traceback: 124 | -- --> stdin:1: in main chunk 125 | -- --> [C]: in ? 126 | local function callable(x) 127 | if type(x) == 'function' then 128 | return x 129 | end 130 | return (getmetatable(x) or {}).__call 131 | end 132 | 133 | 134 | local function case(with, branches) 135 | local match = branches[with] or branches[1] 136 | if callable(match) then 137 | return match(with) 138 | end 139 | return match 140 | end 141 | 142 | 143 | local function collect(ifn, ...) 144 | local r = {} 145 | if not callable(ifn) then 146 | -- No iterator behaves like npairs, which would collect all integer 147 | -- key values from the first argument table: 148 | local k, v = next(ifn) 149 | repeat 150 | if type(k) == 'number' then 151 | r[k] = v 152 | end 153 | k, v = next(ifn, k) 154 | until k == nil 155 | return r 156 | end 157 | 158 | -- Or else result depends on how many return values from ifn? 159 | local arity = 1 160 | for e, v in ifn(...) do 161 | if v then 162 | arity, r = 2, {} 163 | break 164 | end 165 | -- Build an arity-1 result table on first pass... 166 | r[#r + 1] = e 167 | end 168 | 169 | if arity == 2 then 170 | -- ...oops, it was arity-2 all along, start again! 171 | for k, v in ifn(...) do 172 | r[k] = v 173 | end 174 | end 175 | 176 | return r 177 | end 178 | 179 | 180 | local function compose(...) 181 | local fns = pack(...) 182 | 183 | return function(...) 184 | local argt = pack(...) 185 | for i = 1, fns.n do 186 | argt = pack(fns[i](unpack(argt, 1, argt.n))) 187 | end 188 | return unpack(argt, 1, argt.n) 189 | end 190 | end 191 | 192 | 193 | local function cond(expr, branch, ...) 194 | if branch == nil and select('#', ...) == 0 then 195 | expr, branch = true, expr 196 | end 197 | if expr then 198 | if callable(branch) then 199 | return branch(expr) 200 | end 201 | return branch 202 | end 203 | return cond(...) 204 | end 205 | 206 | 207 | local function curry(fn, n) 208 | if n <= 1 then 209 | return fn 210 | else 211 | return function(x) 212 | return curry(bind(fn, {x}), n - 1) 213 | end 214 | end 215 | end 216 | 217 | 218 | local function filter(pfn, ifn, ...) 219 | local argt, r = pack(...), {} 220 | if not callable(ifn) then 221 | ifn, argt = pairs, pack(ifn, ...) 222 | end 223 | 224 | local nextfn, state, k = ifn(unpack(argt, 1, argt.n)) 225 | 226 | local t = pack(nextfn(state, k)) -- table of iteration 1 227 | local arity = #t -- How many return values from ifn? 228 | 229 | if arity == 1 then 230 | local v = t[1] 231 | while v ~= nil do -- until iterator returns nil 232 | if pfn(unpack(t, 1, t.n)) then -- pass all iterator results to p 233 | r[#r + 1] = v 234 | end 235 | 236 | t = pack(nextfn(state, v)) -- maintain loop invariant 237 | v = t[1] 238 | 239 | if #t > 1 then -- unless we discover arity is not 1 after all 240 | arity, r = #t, {} break 241 | end 242 | end 243 | end 244 | 245 | if arity > 1 then 246 | -- No need to start over here, because either: 247 | -- (i) arity was never 1, and the original value of t is correct 248 | -- (ii) arity used to be 1, but we only consumed nil values, so the 249 | -- current t with arity > 1 is the correct next value to use 250 | while t[1] ~= nil do 251 | local k = t[1] 252 | if pfn(unpack(t, 1, t.n)) then 253 | r[k] = t[2] 254 | end 255 | t = pack(nextfn(state, k)) 256 | end 257 | end 258 | 259 | return r 260 | end 261 | 262 | 263 | local function flatten(t) 264 | return collect(leaves, ipairs, t) 265 | end 266 | 267 | 268 | local function reduce(fn, d, ifn, ...) 269 | local argt 270 | if not callable(ifn) then 271 | ifn, argt = pairs, pack(ifn, ...) 272 | else 273 | argt = pack(...) 274 | end 275 | 276 | local nextfn, state, k = ifn(unpack(argt, 1, argt.n)) 277 | local t = pack(nextfn(state, k)) -- table of iteration 1 278 | 279 | local r = d -- initialise accumulator 280 | while t[1] ~= nil do -- until iterator returns nil 281 | k = t[1] 282 | r = fn(r, unpack(t, 1, t.n)) -- pass all iterator results to fn 283 | t = pack(nextfn(state, k)) -- maintain loop invariant 284 | end 285 | return r 286 | end 287 | 288 | 289 | local function foldl(fn, d, t) 290 | if t == nil then 291 | local tail = {} 292 | for i = 2, len(d) do 293 | tail[#tail + 1] = d[i] 294 | end 295 | d, t = d[1], tail 296 | end 297 | return reduce(fn, d, ielems, t) 298 | end 299 | 300 | 301 | -- Be careful to reverse only the valid sequence part of a table. 302 | local function ireverse(t) 303 | local oob = 1 304 | while t[oob] ~= nil do 305 | oob = oob + 1 306 | end 307 | 308 | local r = {} 309 | for i = 1, oob - 1 do 310 | r[oob - i] = t[i] 311 | end 312 | return r 313 | end 314 | 315 | 316 | local function foldr(fn, d, t) 317 | if t == nil then 318 | local u, last = {}, len(d) 319 | for i = 1, last - 1 do 320 | u[#u + 1] = d[i] 321 | end 322 | d, t = d[last], u 323 | end 324 | return reduce(function(x, y) return fn(y, x) end, d, ielems, ireverse(t)) 325 | end 326 | 327 | 328 | local function id(...) 329 | return ... 330 | end 331 | 332 | 333 | local function multiserialize(...) 334 | local seq = pack(...) 335 | local buf = {} 336 | for i = 1, seq.n do 337 | buf[i] = serialize(seq[i]) 338 | end 339 | return concat(buf, ',') 340 | end 341 | 342 | 343 | local function memoize(fn, mnemonic) 344 | mnemonic = mnemonic or multiserialize 345 | 346 | return setmetatable({}, { 347 | __call = function(self, ...) 348 | local k = mnemonic(...) 349 | local t = self[k] 350 | if t == nil then 351 | t = pack(fn(...)) 352 | self[k] = t 353 | end 354 | return unpack(t, 1, t.n) 355 | end 356 | }) 357 | end 358 | 359 | 360 | local lambda = memoize(function(s) 361 | local expr 362 | 363 | -- Support '|args|expression' format. 364 | local args, body = s:match '^%s*|%s*([^|]*)|%s*(.+)%s*$' 365 | if args and body then 366 | expr = 'return function(' .. args .. ') return ' .. body .. ' end' 367 | end 368 | 369 | -- Support 'expression' format. 370 | if not expr then 371 | body = s:match '^%s*(_.*)%s*$' or s:match '^=%s*(.+)%s*$' 372 | if body then 373 | expr = [[ 374 | return function(...) 375 | local _1,_2,_3,_4,_5,_6,_7,_8,_9 = ... 376 | local _ = _1 377 | return ]] .. body .. [[ 378 | end 379 | ]] 380 | end 381 | end 382 | 383 | local ok, fn 384 | if expr then 385 | ok, fn = pcall(load(expr)) 386 | end 387 | 388 | -- Diagnose invalid input. 389 | if not ok then 390 | return nil, "invalid lambda string '" .. s .. "'" 391 | end 392 | 393 | return setmetatable({}, { 394 | __call = function(self, ...) return fn(...) end, 395 | __tostring = function(self) return s end, 396 | }) 397 | end, id) 398 | 399 | 400 | local function map(mapfn, ifn, ...) 401 | local argt, r = pack(...), {} 402 | if not callable(ifn) or argt.n == 0 then 403 | ifn, argt = pairs, pack(ifn, ...) 404 | end 405 | 406 | local nextfn, state, k = ifn(unpack(argt, 1, argt.n)) 407 | local mapargs = pack(nextfn(state, k)) 408 | 409 | local arity = 1 410 | while mapargs[1] ~= nil do 411 | local d, v = mapfn(unpack(mapargs, 1, mapargs.n)) 412 | if v ~= nil then 413 | arity, r = 2, {} break 414 | end 415 | r[#r + 1] = d 416 | mapargs = {nextfn(state, mapargs[1])} 417 | end 418 | 419 | if arity > 1 then 420 | -- No need to start over here, because either: 421 | -- (i) arity was never 1, and the original value of mapargs is correct 422 | -- (ii) arity used to be 1, but we only consumed nil values, so the 423 | -- current mapargs with arity > 1 is the correct next value to use 424 | while mapargs[1] ~= nil do 425 | local k, v = mapfn(unpack(mapargs, 1, mapargs.n)) 426 | r[k] = v 427 | mapargs = pack(nextfn(state, mapargs[1])) 428 | end 429 | end 430 | return r 431 | end 432 | 433 | 434 | local function map_with(mapfn, tt) 435 | local r = {} 436 | for k, v in next, tt do 437 | r[k] = mapfn(unpack(v, 1, len(v))) 438 | end 439 | return r 440 | end 441 | 442 | 443 | local function _product(x, l) 444 | local r = {} 445 | for v1 in ielems(x) do 446 | for v2 in ielems(l) do 447 | r[#r + 1] = {v1, unpack(v2, 1, len(v2))} 448 | end 449 | end 450 | return r 451 | end 452 | 453 | local function product(...) 454 | local argt = {...} 455 | if not next(argt) then 456 | return argt 457 | else 458 | -- Accumulate a list of products, starting by popping the last 459 | -- argument and making each member a one element list. 460 | local d = map(lambda '={_1}', ielems, remove(argt)) 461 | -- Right associatively fold in remaining argt members. 462 | return foldr(_product, d, argt) 463 | end 464 | end 465 | 466 | 467 | local function shape(dims, t) 468 | t = flatten(t) 469 | -- Check the shape and calculate the size of the zero, if any 470 | local size = 1 471 | local zero 472 | for i, v in ipairs(dims) do 473 | if v == 0 then 474 | if zero then -- bad shape: two zeros 475 | return nil 476 | else 477 | zero = i 478 | end 479 | else 480 | size = size * v 481 | end 482 | end 483 | if zero then 484 | dims[zero] = ceil(len(t) / size) 485 | end 486 | local function fill(i, d) 487 | if d > len(dims) then 488 | return t[i], i + 1 489 | else 490 | local r = {} 491 | for j = 1, dims[d] do 492 | local e 493 | e, i = fill(i, d + 1) 494 | r[#r + 1] = e 495 | end 496 | return r, i 497 | end 498 | end 499 | return(fill(1, 1)) 500 | end 501 | 502 | 503 | local function zip(tt) 504 | local r = {} 505 | for outerk, inner in pairs(tt) do 506 | for k, v in pairs(inner) do 507 | r[k] = r[k] or {} 508 | r[k][outerk] = v 509 | end 510 | end 511 | return r 512 | end 513 | 514 | 515 | local function zip_with(fn, tt) 516 | return map_with(fn, zip(tt)) 517 | end 518 | 519 | 520 | 521 | --[[ ================= ]]-- 522 | --[[ Public Interface. ]]-- 523 | --[[ ================= ]]-- 524 | 525 | 526 | local function X(decl, fn) 527 | return argscheck('std.functional.' .. decl, fn) 528 | end 529 | 530 | return { 531 | --- Call a series of functions until one returns non-nil. 532 | -- @function any 533 | -- @func ... functions to call 534 | -- @treturn function to call fn1 .. fnN until one returns non-nil. 535 | -- @usage 536 | -- old_object_type = any(std.object.type, io.type, type) 537 | any = X('any(func...)', any), 538 | 539 | --- Partially apply a function. 540 | -- @function bind 541 | -- @func fn function to apply partially 542 | -- @tparam table argt table of *fn* arguments to bind 543 | -- @return function with *argt* arguments already bound 544 | -- @usage 545 | -- cube = bind(std.functional.operator.pow, {[2] = 3}) 546 | bind = X('bind(func, table)', bind), 547 | 548 | --- Identify callable types. 549 | -- @function callable 550 | -- @param x an object or primitive 551 | -- @return `true` if *x* can be called, otherwise `false` 552 | -- @usage 553 | -- if callable(functable) then functable(args) end 554 | callable = X('callable(?any)', callable), 555 | 556 | --- A rudimentary case statement. 557 | -- Match *with* against keys in *branches* table. 558 | -- @function case 559 | -- @param with expression to match 560 | -- @tparam table branches map possible matches to functions 561 | -- @return the value associated with a matching key, or the first non-key 562 | -- value if no key matches. Function or functable valued matches are 563 | -- called using *with* as the sole argument, and the result of that call 564 | -- returned; otherwise the matching value associated with the matching 565 | -- key is returned directly; or else `nil` if there is no match and no 566 | -- default. 567 | -- @see cond 568 | -- @usage 569 | -- return case(type(object), { 570 | -- table = 'table', 571 | -- string = function() return 'string' end, 572 | -- function(s) error('unhandled type: ' .. s) end, 573 | -- }) 574 | case = X('case(?any, #table)', case), 575 | 576 | --- Collect the results of an iterator. 577 | -- @function collect 578 | -- @func[opt=std.npairs] ifn iterator function 579 | -- @param ... *ifn* arguments 580 | -- @treturn table of results from running *ifn* on *args* 581 | -- @see filter 582 | -- @see map 583 | -- @usage 584 | -- --> {'a', 'b', 'c'} 585 | -- collect {'a', 'b', 'c', x=1, y=2, z=5} 586 | collect = X('collect([func], ?any...)', collect), 587 | 588 | --- Compose functions. 589 | -- @function compose 590 | -- @func ... functions to compose 591 | -- @treturn function composition of fnN .. fn1: note that this is the 592 | -- reverse of what you might expect, but means that code like: 593 | -- 594 | -- compose(function(x) return f(x) end, 595 | -- function(x) return g(x) end)) 596 | -- 597 | -- can be read from top to bottom. 598 | -- @usage 599 | -- vpairs = compose(table.invert, ipairs) 600 | -- for v, i in vpairs {'a', 'b', 'c'} do process(v, i) end 601 | compose = X('compose(func...)', compose), 602 | 603 | --- A rudimentary condition-case statement. 604 | -- If *expr* is 'truthy' return *branch* if given, otherwise *expr* 605 | -- itself. If the return value is a function or functable, then call it 606 | -- with *expr* as the sole argument and return the result; otherwise 607 | -- return it explicitly. If *expr* is 'falsey', then recurse with the 608 | -- first two arguments stripped. 609 | -- @function cond 610 | -- @param expr a Lua expression 611 | -- @param branch a function, functable or value to use if *expr* is 612 | -- 'truthy' 613 | -- @param ... additional arguments to retry if *expr* is 'falsey' 614 | -- @see case 615 | -- @usage 616 | -- -- recursively calculate the nth triangular number 617 | -- function triangle(n) 618 | -- return cond( 619 | -- n <= 0, 0, 620 | -- n == 1, 1, 621 | -- function() return n + triangle(n - 1) end) 622 | -- end 623 | cond = cond, -- any number of any type arguments! 624 | 625 | --- Curry a function. 626 | -- @function curry 627 | -- @func fn function to curry 628 | -- @int n number of arguments 629 | -- @treturn function curried version of *fn* 630 | -- @usage 631 | -- add = curry(function(x, y) return x + y end, 2) 632 | -- incr, decr = add(1), add(-1) 633 | curry = X('curry(func, int)', curry), 634 | 635 | --- Filter an iterator with a predicate. 636 | -- @function filter 637 | -- @tparam predicate pfn predicate function 638 | -- @func[opt=std.pairs] ifn iterator function 639 | -- @param ... iterator arguments 640 | -- @treturn table elements e for which `pfn(e)` is not 'falsey'. 641 | -- @see collect 642 | -- @see map 643 | -- @usage 644 | -- --> {2, 4} 645 | -- filter(lambda '|e|e%2==0', std.elems, {1, 2, 3, 4}) 646 | filter = X('filter(func, [func], ?any...)', filter), 647 | 648 | --- Flatten a nested table into a list. 649 | -- @function flatten 650 | -- @tparam table t a table 651 | -- @treturn table a list of all non-table elements of *t* 652 | -- @usage 653 | -- --> {1, 2, 3, 4, 5} 654 | -- flatten {{1, {{2}, 3}, 4}, 5} 655 | flatten = X('flatten(table)', flatten), 656 | 657 | --- Fold a binary function left associatively. 658 | -- If parameter *d* is omitted, the first element of *t* is used, 659 | -- and *t* treated as if it had been passed without that element. 660 | -- @function foldl 661 | -- @func fn binary function 662 | -- @param[opt=t[1]] d initial left-most argument 663 | -- @tparam table t a table 664 | -- @return result 665 | -- @see foldr 666 | -- @see reduce 667 | -- @usage 668 | -- foldl(operator.quot, {10000, 100, 10}) == (10000 / 100) / 10 669 | foldl = X('foldl(function, [any], table)', foldl), 670 | 671 | --- Fold a binary function right associatively. 672 | -- If parameter *d* is omitted, the last element of *t* is used, 673 | -- and *t* treated as if it had been passed without that element. 674 | -- @function foldr 675 | -- @func fn binary function 676 | -- @param[opt=t[1]] d initial right-most argument 677 | -- @tparam table t a table 678 | -- @return result 679 | -- @see foldl 680 | -- @see reduce 681 | -- @usage 682 | -- foldr(operator.quot, {10000, 100, 10}) == 10000 / (100 / 10) 683 | foldr = X('foldr(function, [any], table)', foldr), 684 | 685 | --- Identity function. 686 | -- @function id 687 | -- @param ... arguments 688 | -- @return *arguments* 689 | id = id, -- any number of any type arguments! 690 | 691 | --- Return a new sequence with element order reversed. 692 | -- 693 | -- Apart from the order of the elements returned, this function follows 694 | -- the same rules as @{ipairs} for determining first and last elements. 695 | -- @function ireverse 696 | -- @tparam table t a table 697 | -- @treturn table a new table with integer keyed elements in reverse 698 | -- order with respect to *t* 699 | -- @see ipairs 700 | -- @usage 701 | -- local rielems = compose(ireverse, std.ielems) 702 | -- --> bar 703 | -- --> foo 704 | -- map(print, rielems, {'foo', 'bar', [4]='baz', d=5}) 705 | ireverse = X('ireverse(table)', ireverse), 706 | 707 | --- Compile a lambda string into a Lua function. 708 | -- 709 | -- A valid lambda string takes one of the following forms: 710 | -- 711 | -- 1. `'=expression'`: equivalent to `function(...) return expression end` 712 | -- 1. `'|args|expression'`: equivalent to `function(args) return expression end` 713 | -- 714 | -- The first form (starting with `'='`) automatically assigns the first 715 | -- nine arguments to parameters `'_1'` through `'_9'` for use within the 716 | -- expression body. The parameter `'_1'` is aliased to `'_'`, and if the 717 | -- first non-whitespace of the whole expression is `'_'`, then the 718 | -- leading `'='` can be omitted. 719 | -- 720 | -- The results are memoized, so recompiling a previously compiled 721 | -- lambda string is extremely fast. 722 | -- @function lambda 723 | -- @string s a lambda string 724 | -- @treturn functable compiled lambda string, can be called like a function 725 | -- @usage 726 | -- -- The following are equivalent: 727 | -- lambda '= _1 < _2' 728 | -- lambda '|a,b| a {1, 4, 9, 16} 742 | -- map(lambda '=_1*_1', std.ielems, {1, 2, 3, 4}) 743 | map = X('map(func, [func], ?any...)', map), 744 | 745 | --- Map a function over a table of argument lists. 746 | -- @function map_with 747 | -- @func fn map function 748 | -- @tparam table tt a table of *fn* argument lists 749 | -- @treturn table new table of *fn* results 750 | -- @see map 751 | -- @see zip_with 752 | -- @usage 753 | -- --> {'123', '45'}, {a='123', b='45'} 754 | -- conc = bind(map_with, {lambda '|...|table.concat {...}'}) 755 | -- conc {{1, 2, 3}, {4, 5}}, conc {a={1, 2, 3, x='y'}, b={4, 5, z=6}} 756 | map_with = X('map_with(function, table of tables)', map_with), 757 | 758 | --- Memoize a function, by wrapping it in a functable. 759 | -- 760 | -- To ensure that memoize always returns the same results for the same 761 | -- arguments, it passes arguments to *fn*. You can specify a more 762 | -- sophisticated function if memoize should handle complicated argument 763 | -- equivalencies. 764 | -- @function memoize 765 | -- @func fn pure function: a function with no side effects 766 | -- @tparam[opt=std.tostring] mnemonic mnemonicfn how to remember the arguments 767 | -- @treturn functable memoized function 768 | -- @usage 769 | -- local fast = memoize(function(...) --[[ slow code ]] end) 770 | memoize = X('memoize(func, ?func)', memoize), 771 | 772 | --- No operation. 773 | -- This function ignores all arguments, and returns no values. 774 | -- @function nop 775 | -- @see id 776 | -- @usage 777 | -- if unsupported then vtable['memrmem'] = nop end 778 | nop = function() end, 779 | 780 | --- Functional list product. 781 | -- 782 | -- Return a list of each combination possible by taking a single 783 | -- element from each of the argument lists. 784 | -- @function product 785 | -- @param ... operands 786 | -- @return result 787 | -- @usage 788 | -- --> {'000', '001', '010', '011', '100', '101', '110', '111'} 789 | -- map(table.concat, ielems, product({0,1}, {0, 1}, {0, 1})) 790 | product = X('product(list...)', product), 791 | 792 | --- Fold a binary function into an iterator. 793 | -- @function reduce 794 | -- @func fn reduce function 795 | -- @param d initial first argument 796 | -- @func[opt=std.pairs] ifn iterator function 797 | -- @param ... iterator arguments 798 | -- @return result 799 | -- @see foldl 800 | -- @see foldr 801 | -- @usage 802 | -- --> 2 ^ 3 ^ 4 ==> 4096 803 | -- reduce(operator.pow, 2, std.ielems, {3, 4}) 804 | reduce = X('reduce(func, any, [func], ?any...)', reduce), 805 | 806 | --- Shape a table according to a list of dimensions. 807 | -- 808 | -- Dimensions are given outermost first and items from the original 809 | -- list are distributed breadth first; there may be one 0 indicating 810 | -- an indefinite number. Hence, `{0}` is a flat list, 811 | -- `{1}` is a singleton, `{2, 0}` is a list of 812 | -- two lists, and `{0, 2}` is a list of pairs. 813 | -- 814 | -- Algorithm: turn shape into all positive numbers, calculating 815 | -- the zero if necessary and making sure there is at most one; 816 | -- recursively walk the shape, adding empty tables until the bottom 817 | -- level is reached at which point add table items instead, using a 818 | -- counter to walk the flattened original list. 819 | -- 820 | -- @todo Use ileaves instead of flatten (needs a while instead of a 821 | -- for in fill function) 822 | -- @function shape 823 | -- @tparam table dims table of dimensions `{d1, ..., dn}` 824 | -- @tparam table t a table of elements 825 | -- @return reshaped list 826 | -- @usage 827 | -- --> {{'a', 'b'}, {'c', 'd'}, {'e', 'f'}} 828 | -- shape({3, 2}, {'a', 'b', 'c', 'd', 'e', 'f'}) 829 | shape = X('shape(table, table)', shape), 830 | 831 | --- Zip a table of tables. 832 | -- Make a new table, with lists of elements at the same index in the 833 | -- original table. This function is effectively its own inverse. 834 | -- @function zip 835 | -- @tparam table tt a table of tables 836 | -- @treturn table new table with lists of elements of the same key 837 | -- from *tt* 838 | -- @see map 839 | -- @see zip_with 840 | -- @usage 841 | -- --> {{1, 3, 5}, {2, 4}}, {a={x=1, y=3, z=5}, b={x=2, y=4}} 842 | -- zip {{1, 2}, {3, 4}, {5}}, zip {x={a=1, b=2}, y={a=3, b=4}, z={a=5}} 843 | zip = X('zip(table of tables)', zip), 844 | 845 | --- Zip a list of tables together with a function. 846 | -- @function zip_with 847 | -- @tparam function fn function 848 | -- @tparam table tt table of tables 849 | -- @treturn table a new table of results from calls to *fn* with arguments 850 | -- made from all elements the same key in the original tables; effectively 851 | -- the 'columns' in a simple list 852 | -- of lists. 853 | -- @see map_with 854 | -- @see zip 855 | -- @usage 856 | -- --> {'135', '24'}, {a='1', b='25'} 857 | -- conc = bind(zip_with, {lambda '|...|table.concat {...}'}) 858 | -- conc {{1, 2}, {3, 4}, {5}}, conc {{a=1, b=2}, x={a=3, b=4}, {b=5}} 859 | zip_with = X('zip_with(function, table of tables)', zip_with), 860 | } 861 | 862 | 863 | --- Types 864 | -- @section Types 865 | 866 | 867 | --- Signature of a @{memoize} argument normalization callback function. 868 | -- @function mnemonic 869 | -- @param ... arguments 870 | -- @treturn string stable serialized arguments 871 | -- @usage 872 | -- local mnemonic = function(name, value, props) return name end 873 | -- local intern = memoize(mksymbol, mnemonic) 874 | 875 | 876 | --- Signature of a @{filter} predicate callback function. 877 | -- @function predicate 878 | -- @param ... arguments 879 | -- @treturn boolean 'truthy' if the predicate condition succeeds, 880 | -- 'falsey' otherwise 881 | -- @usage 882 | -- local predicate = lambda '|k,v|type(v)=="string"' 883 | -- local strvalues = filter(predicate, std.pairs, {name='Roberto', id=12345}) 884 | -------------------------------------------------------------------------------- /lib/std/functional/operator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2002-2022 std.functional authors 4 | ]] 5 | --[[-- 6 | Functional forms of Lua operators. 7 | 8 | @module std.functional.operator 9 | ]] 10 | 11 | 12 | local _ENV = require 'std.normalize' { 13 | 'std.functional._base.serialize', 14 | } 15 | 16 | 17 | --[[ =============== ]]-- 18 | --[[ Implementation. ]]-- 19 | --[[ =============== ]]-- 20 | 21 | 22 | local function eqv(a, b) 23 | -- If they are the same primitive value, or they share a metatable 24 | -- with an __eq metamethod that says they are equivalent, we're done! 25 | if a == b then 26 | return true 27 | end 28 | 29 | -- Unless we now have two tables, the previous line ensures a ~= b. 30 | if type(a) ~= 'table' or type(b) ~= 'table' then 31 | return false 32 | end 33 | 34 | -- Otherwise, compare serializations of each. 35 | return serialize(a) == serialize(b) 36 | end 37 | 38 | 39 | return { 40 | --- Stringify and concatenate arguments. 41 | -- @param a an argument 42 | -- @param b another argument 43 | -- @return concatenation of stringified arguments. 44 | -- @usage 45 | -- --> '=> 1000010010' 46 | -- foldl(concat, '=> ', {10000, 100, 10}) 47 | concat = function(a, b) 48 | return tostring(a) .. tostring(b) 49 | end, 50 | 51 | --- Equivalent to `#` operation, but respecting `__len` even on Lua 5.1. 52 | -- @function len 53 | -- @tparam object|string|table x operand 54 | -- @treturn int length of list part of *t* 55 | -- @usage 56 | -- for i = 1, len(t) do process(t[i]) end 57 | len = len, 58 | 59 | --- Dereference a table. 60 | -- @tparam table t a table 61 | -- @param k a key to lookup in *t* 62 | -- @return value stored at *t[k]* if any, otherwise `nil` 63 | -- @usage 64 | -- --> 4 65 | -- foldl(get, {1, {{2, 3, 4}, 5}}, {2, 1, 3}) 66 | get = function(t, k) 67 | return t and t[k] or nil 68 | end, 69 | 70 | --- Set a table element, honoring metamethods. 71 | -- @tparam table t a table 72 | -- @param k a key to lookup in *t* 73 | -- @param v a value to set for *k* 74 | -- @treturn table *t* 75 | -- @usage 76 | -- -- destructive table merge: 77 | -- --> {'one', bar='baz', two=5} 78 | -- reduce(set, {'foo', bar='baz'}, {'one', two=5}) 79 | set = function(t, k, v) 80 | t[k]=v 81 | return t 82 | end, 83 | 84 | --- Return the sum of the arguments. 85 | -- @param a an argument 86 | -- @param b another argument 87 | -- @return the sum of the *a* and *b* 88 | -- @usage 89 | -- --> 10110 90 | -- foldl(sum, {10000, 100, 10}) 91 | sum = function(a, b) 92 | return a + b 93 | end, 94 | 95 | --- Return the difference of the arguments. 96 | -- @param a an argument 97 | -- @param b another argument 98 | -- @return the difference between *a* and *b* 99 | -- @usage 100 | -- --> 890 101 | -- foldl(diff, {10000, 100, 10}) 102 | diff = function(a, b) 103 | return a - b 104 | end, 105 | 106 | --- Return the product of the arguments. 107 | -- @param a an argument 108 | -- @param b another argument 109 | -- @return the product of *a* and *b* 110 | -- @usage 111 | -- --> 10000000 112 | -- foldl(prod, {10000, 100, 10}) 113 | prod = function(a, b) 114 | return a * b 115 | end, 116 | 117 | --- Return the quotient of the arguments. 118 | -- @param a an argument 119 | -- @param b another argument 120 | -- @return the quotient *a* and *b* 121 | -- @usage 122 | -- --> 1000 123 | -- foldr(quot, {10000, 100, 10}) 124 | quot = function(a, b) 125 | return a / b 126 | end, 127 | 128 | --- Return the modulus of the arguments. 129 | -- @param a an argument 130 | -- @param b another argument 131 | -- @return the modulus of *a* and *b* 132 | -- @usage 133 | -- --> 3 134 | -- foldl(mod, {65536, 100, 11}) 135 | mod = function(a, b) 136 | return a % b 137 | end, 138 | 139 | --- Return the exponent of the arguments. 140 | -- @param a an argument 141 | -- @param b another argument 142 | -- @return the *a* to the power of *b* 143 | -- @usage 144 | -- --> 4096 145 | -- foldl(pow, {2, 3, 4}) 146 | pow = function(a, b) 147 | return a ^ b 148 | end, 149 | 150 | --- Return the logical conjunction of the arguments. 151 | -- @param a an argument 152 | -- @param b another argument 153 | -- @return logical *a* and *b* 154 | -- @usage 155 | -- --> true 156 | -- foldl(conj, {true, 1, 'false'}) 157 | conj = function(a, b) 158 | return a and b 159 | end, 160 | 161 | --- Return the logical disjunction of the arguments. 162 | -- @param a an argument 163 | -- @param b another argument 164 | -- @return logical *a* or *b* 165 | -- @usage 166 | -- --> true 167 | -- foldl(disj, {true, 1, false}) 168 | disj = function(a, b) 169 | return a or b 170 | end, 171 | 172 | --- Return the logical negation of the arguments. 173 | -- @param a an argument 174 | -- @return not *a* 175 | -- @usage 176 | -- --> {true, false, false, false} 177 | -- bind(map, {std.ielems, neg}) {false, true, 1, 0} 178 | neg = function(a) 179 | return not a 180 | end, 181 | 182 | --- Recursive table equivalence. 183 | -- @param a an argument 184 | -- @param b another argument 185 | -- @treturn boolean whether *a* and *b* are recursively equivalent 186 | eqv = eqv, 187 | 188 | --- Return the equality of the arguments. 189 | -- @param a an argument 190 | -- @param b another argument 191 | -- @return `true` if *a* is *b*, otherwise `false` 192 | eq = function(a, b) 193 | return a == b 194 | end, 195 | 196 | --- Return the inequality of the arguments. 197 | -- @param a an argument 198 | -- @param b another argument 199 | -- @return `false` if *a* is *b*, otherwise `true` 200 | -- @usage 201 | -- --> true 202 | -- local f = require 'std.functional' 203 | -- table.empty(f.filter (f.bind(neq, {6}), std.ielems, {6, 6, 6}) 204 | neq = function(a, b) 205 | return a ~= b 206 | end, 207 | 208 | --- Return whether the arguments are in ascending order. 209 | -- @param a an argument 210 | -- @param b another argument 211 | -- @return `true` if *a* is less then *b*, otherwise `false` 212 | lt = function(a, b) 213 | return a < b 214 | end, 215 | 216 | --- Return whether the arguments are not in descending order. 217 | -- @param a an argument 218 | -- @param b another argument 219 | -- @return `true` if *a* is not greater then *b*, otherwise `false` 220 | lte = function(a, b) 221 | return a <= b 222 | end, 223 | 224 | --- Return whether the arguments are in descending order. 225 | -- @param a an argument 226 | -- @param b another argument 227 | -- @return `true` if *a* is greater then *b*, otherwise `false` 228 | gt = function(a, b) 229 | return a > b 230 | end, 231 | 232 | --- Return whether the arguments are not in ascending order. 233 | -- @param a an argument 234 | -- @param b another argument 235 | -- @return `true` if *a* is not greater then *b*, otherwise `false` 236 | gte = function(a, b) 237 | return a >= b 238 | end, 239 | } 240 | -------------------------------------------------------------------------------- /lib/std/functional/tuple.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2002-2022 std.functional authors 4 | ]] 5 | --[[-- 6 | Tuple container. 7 | 8 | An interned, immutable, nil-preserving tuple object. 9 | 10 | Like Lua strings, tuples with the same elements can be quickly compared 11 | with a straight-forward `==` comparison. 12 | 13 | The immutability guarantees only work if you don't change the contents 14 | of tables after adding them to a tuple. Don't do that! 15 | 16 | @module std.functional.tuple 17 | ]] 18 | 19 | 20 | local _ENV = require 'std.normalize' { 21 | 'std.functional._base.toqstring', 22 | 'string.format', 23 | 'table.concat', 24 | } 25 | 26 | 27 | 28 | --[[ =============== ]]-- 29 | --[[ Implementation. ]]-- 30 | --[[ =============== ]]-- 31 | 32 | 33 | -- We maintain a weak table of all distinct Tuples, where each value is 34 | -- the tuple object itself, and the associated key is the stringified 35 | -- list of elements contained by the tuple, e.g the 0-tuple: 36 | -- 37 | -- intern[''] = setmetatable({n = 0}, Tuple) 38 | -- 39 | -- In order to make a tuple immutable, it needs to have a __newindex 40 | -- metamethod that diagnoses attempts to insert new elements, which in 41 | -- turn means that the actual elements need to be kept in a proxy table 42 | -- (because if we stored the first element in the tuple table, then 43 | -- __newindex would not fire when the first element was written to). 44 | -- Rather that using metamethods to access a wholly separate proxy 45 | -- table, we use the proxey table as a key in the tuple object proper 46 | -- (it would be impossible to accidentally assign to a unique table 47 | -- address key, so __newindex will still work) and use a copy of the 48 | -- intern stringified elements key as the associated value there, e.g. 49 | -- for the 0-tuple again: 50 | -- 51 | -- { [{n = 0}] = '' } 52 | -- 53 | -- This means we have a pleasant property to enable fast stringification 54 | -- of any tuple we hold: 55 | -- 56 | -- proxy_table, stringified_element_list = next(tuple) 57 | 58 | 59 | --- Immutable Tuple container. 60 | -- @object Tuple 61 | -- @string[opt='Tuple'] _type object name 62 | -- @int n number of tuple elements 63 | -- @usage 64 | -- local Tuple = require 'std.functional.tuple' 65 | -- function count(...) 66 | -- argtuple = Tuple(...) 67 | -- return argtuple.n 68 | -- end 69 | -- count() --> 0 70 | -- count(nil) --> 1 71 | -- count(false) --> 1 72 | -- count(false, nil, true, nil) --> 4 73 | local Tuple = { 74 | _type = 'Tuple', 75 | 76 | --- Metamethods 77 | -- @section metamethods 78 | 79 | --- Unpack tuple values between index *i* and *j*, inclusive. 80 | -- @function __call 81 | -- @int[opt=1] i first index to unpack 82 | -- @int[opt=self.n] j last index to unpack 83 | -- @return ... values at indices *i* through *j*, inclusive 84 | -- @usage 85 | -- tup = Tuple(1, 3, 2, 5) 86 | -- --> 3, 2, 5 87 | -- tup(2) 88 | __call = function(self, i, j) 89 | return unpack(next(self), tonumber(i) or 1, tonumber(j) or self.n) 90 | end, 91 | 92 | __index = function(self, k) 93 | return next(self)[k] 94 | end, 95 | 96 | --- Return the length of this tuple. 97 | -- @function prototype:__len 98 | -- @treturn int number of elements in *tup* 99 | -- @usage 100 | -- -- Only works on Lua 5.2 or newer: 101 | -- #Tuple(nil, 2, nil) --> 3 102 | -- -- For compatibility with Lua 5.1, use @{operator.len} 103 | -- len(Tuple(nil, 2, nil) 104 | __len = function(self) 105 | return self.n 106 | end, 107 | 108 | --- Prevent mutation of *tup*. 109 | -- @function prototype:__newindex 110 | -- @param k tuple key 111 | -- @param v tuple value 112 | -- @raise cannot change immutable tuple object 113 | __newindex = function(self, k, v) 114 | error('cannot change immutable tuple object', 2) 115 | end, 116 | 117 | --- Return a string representation of *tup* 118 | -- @function prototype:__tostring 119 | -- @treturn string representation of *tup* 120 | -- @usage 121 | -- -- "Tuple('nil', nil, false)" 122 | -- print(Tuple('nil', nil, false)) 123 | __tostring = function(self) 124 | local _, argstr = next(self) 125 | return format('%s(%s)', getmetatable(self)._type, argstr) 126 | end, 127 | } 128 | 129 | 130 | -- Maintain a weak functable of all interned tuples. 131 | -- @function intern 132 | -- @int n number of elements in *t*, including trailing `nil`s 133 | -- @tparam table t table of elements 134 | -- @treturn table interned *n*-tuple *t* 135 | local intern = setmetatable({}, { 136 | __mode = 'kv', 137 | 138 | __call = function(self, k, t) 139 | if self[k] == nil then 140 | self[k] = setmetatable({[t] = k}, Tuple) 141 | end 142 | return self[k] 143 | end, 144 | }) 145 | 146 | 147 | -- Call the value returned from requiring this module with a list of 148 | -- elements to get an interned tuple made with those elements. 149 | return function(...) 150 | local n = select('#', ...) 151 | local buf, tup = {}, {n = n, ...} 152 | for i = 1, n do 153 | buf[i] = toqstring(tup[i]) 154 | end 155 | return intern(concat(buf, ', '), tup) 156 | end 157 | -------------------------------------------------------------------------------- /spec/functional_spec.yaml: -------------------------------------------------------------------------------- 1 | # Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2022 std.functional authors 3 | 4 | before: 5 | this_module = 'std.functional.init' 6 | global_table = '_G' 7 | 8 | exported_apis = { 9 | 'any', 'bind', 'callable', 'case', 'collect', 'compose', 'cond', 10 | 'curry', 'filter', 'flatten', 'foldl', 'foldr', 'id', 'ireverse', 11 | 'lambda', 'map', 'map_with', 'memoize', 'nop', 'product', 'reduce', 12 | 'shape', 'zip', 'zip_with', 13 | } 14 | 15 | function getmetamethod(x, n) 16 | local m = (getmetatable(x) or {})[n] 17 | if type(m) == 'function' then 18 | return m 19 | end 20 | return (getmetatable(m) or {}).__call 21 | end 22 | 23 | ipairs = (_VERSION == 'Lua 5.3') and ipairs or function(l) 24 | return function(l, n) 25 | n = n + 1 26 | if l[n] ~= nil then 27 | return n, l[n] 28 | end 29 | end, l, 0 30 | end 31 | 32 | if not pairs(setmetatable({},{__pairs=function() return false end})) then 33 | local _pairs = pairs 34 | pairs = function(t) 35 | return (getmetamethod(t, '__pairs') or _pairs)(t) 36 | end 37 | end 38 | 39 | function elems(t) 40 | local fn, istate, ctrl = pairs(t) 41 | return function(state, _) 42 | local v 43 | ctrl, v = fn(state, ctrl) 44 | if ctrl then return v end 45 | end, istate, true 46 | end 47 | 48 | function ielems(t) 49 | local fn, istate, ctrl = ipairs(t) 50 | return function(state, _) 51 | local v 52 | ctrl, v = fn(state, ctrl) 53 | if ctrl then return v end 54 | end, istate, true -- wrapped initial state 55 | end 56 | 57 | maxn = table.maxn or function(t) 58 | local n = 0 59 | for k in pairs(t) do 60 | if type(k) == 'number' and k > n then n = k end 61 | end 62 | return n 63 | end 64 | 65 | function npairs(t) 66 | local m = getmetamethod(t, '__len') 67 | local i, n = 0, m and m(t) or maxn(t) 68 | return function(t) 69 | i = i + 1 70 | if i <= n then return i, t[i] end 71 | end, 72 | t, i 73 | end 74 | 75 | function iter(...) 76 | local l = pack(...) 77 | local oob = l.n + 1 78 | return function(l, n) 79 | n = n - 1 80 | if n > 0 then return n, l[n] or false end 81 | end, l, oob 82 | end 83 | 84 | M = require(this_module) 85 | 86 | 87 | specify functional: 88 | - context when required: 89 | - context by name: 90 | - it does not touch the global table: 91 | expect(show_apis {added_to=global_table, by=this_module}). 92 | to_equal {} 93 | - it exports the documented apis: 94 | t = {} 95 | for k in pairs(M) do t[#t + 1] = k end 96 | expect(t).to_contain.a_permutation_of(exported_apis) 97 | 98 | 99 | - describe any: 100 | - before: 101 | stop = function() return true end 102 | fail = function() return false end 103 | 104 | f = M.any 105 | 106 | - context with bad arguments: 107 | badargs.diagnose(f, 'std.functional.any(func*)') 108 | 109 | - it returns a single function: 110 | expect(type(f(M.id))).to_be 'function' 111 | - it propagates arguments and returned results: 112 | expect(f(M.id)(true)).to_be(true) 113 | expect({f(M.id)(1, 2, 3)}).to_equal {1, 2, 3} 114 | - it propagates nil arguments correctly: 115 | truthy = function(x) return x and true or false end 116 | expect(f(truthy)(true, false, nil, nil, 0, nil)). 117 | to_equal(true, false, false, false, true, false) 118 | - it calls all functions until one returns non-nil: 119 | expect(f(M.id, M.id, stop, fail)(nil)).to_be(true) 120 | - it only looks at first returned value: 121 | expect(f(M.id, M.id, stop, fail)(nil, 'ignored')).to_be(true) 122 | - it does not call remaining functions after non-nil return: 123 | expect(f(M.id, fail)(true)).to_be(true) 124 | 125 | 126 | - describe bind: 127 | - before: 128 | op = require 'std.functional.operator' 129 | 130 | f = M.bind 131 | 132 | - context with bad arguments: 133 | badargs.diagnose(f, 'std.functional.bind(function, table)') 134 | 135 | - it does not affect normal operation if no arguments are bound: 136 | expect(f(math.min, {})(2, 3, 4)).to_be(2) 137 | - it takes the extra arguments into account: 138 | expect(f(math.min, {1, 0})(2, 3, 4)).to_be(0) 139 | - it appends final call arguments: 140 | expect(f(math.max, {2, 3})(4, 5, 1)).to_be(5) 141 | - it does not require all arguments in final call: 142 | div = function(a, b) return a / b end 143 | expect(f(div, {100})(25)).to_be(4) 144 | - it supports out of order extra arguments: 145 | expect(f(op.pow, {[2] = 3})(2)).to_be(8) 146 | - it propagates nil arguments correctly: 147 | r = pack(f(M.id, {[4]='d'})(1)) 148 | expect(r).to_equal(pack(1, nil, nil, 'd')) 149 | r = pack(f(M.id, {[4]='d'})(nil, 2)) 150 | expect(r).to_equal(pack(nil, 2, nil, 'd')) 151 | r = pack(f(M.id, {[2]='b', [4]='d'})(nil, 3, 5, 6, nil)) 152 | expect(r).to_equal(pack(nil, 'b', 3, 'd', 5, 6, nil)) 153 | r = pack(f(M.id, {[2]='b', [4]='d'})(nil, 3, nil, nil, 7)) 154 | expect(r).to_equal(pack(nil, 'b', 3, 'd', nil, nil, 7)) 155 | r = pack(f(M.id, {[2]='b', [4]='d'})(nil, 3, nil, nil, 7, nil, nil)) 156 | expect(r).to_equal(pack(nil, 'b', 3, 'd', nil, nil, 7, nil, nil)) 157 | 158 | 159 | - describe callable: 160 | - before: 161 | f = M.callable 162 | 163 | - context with bad arguments: 164 | badargs.diagnose(f, 'std.functional.callable(?any)') 165 | 166 | - it returns the function associated with a callable: 167 | Container = require 'std.container' { __call=M.nop } 168 | for _, v in ipairs { 169 | true, 170 | 42, 171 | 'str', 172 | io.stderr, 173 | {}, 174 | M.nop, 175 | setmetatable({}, {__call = M.nop}), 176 | Container, 177 | } do 178 | expect(f(v)).to_be(pcall(v, {}) and M.nop or nil) 179 | end 180 | - it returns 'nil' for uncallable arguments: 181 | expect(f()).to_be(nil) 182 | expect(f {}).to_be(nil) 183 | expect(f '').to_be(nil) 184 | 185 | 186 | - describe case: 187 | - before: 188 | yes = function() return true end 189 | no = function() return false end 190 | default = function(s) return s end 191 | branches = {yes=yes, no=no, default} 192 | 193 | f = M.case 194 | 195 | - context with bad arguments: | 196 | badargs.diagnose(f, 'std.functional.case(?any, #table)') 197 | 198 | - it matches against branch keys: 199 | expect(f('yes', branches)).to_be(true) 200 | expect(f('no', branches)).to_be(false) 201 | - it has a default for unmatched keys: 202 | expect(f('none', branches)).to_be 'none' 203 | - it returns nil for unmatched keys with no default: 204 | expect(f('none', {yes=yes, no=no})).to_be(nil) 205 | - it returns non-function matches: 206 | expect(f('t', {t=true})).to_be(true) 207 | - it evaluates returned functions: 208 | expect(f('fn', {fn = function() return true end})). 209 | to_be(true) 210 | - it passes 'with' to function matches: 211 | expect(f('with', {function(s) return s end})).to_be 'with' 212 | - it evaluates returned functables: 213 | functable = setmetatable({}, {__call = function(t, with) return with end}) 214 | expect(f('functable', {functable})).to_be 'functable' 215 | - it evaluates 'with' exactly once: 216 | s = 'prince' 217 | function acc() s = s .. 's'; return s end 218 | expect(f(acc(), { 219 | prince = function() return 'one' end, 220 | princes = function() return 'many' end, 221 | princess = function() return 'one' end, 222 | function() return 'gibberish' end, 223 | })).to_be 'many' 224 | 225 | 226 | - describe collect: 227 | - before: 228 | f = M.collect 229 | 230 | - context with bad arguments: 231 | badargs.diagnose(f, 'std.functional.collect([func], ?any*)') 232 | 233 | - it collects a list of single return value iterator results: 234 | expect(f(ielems, {'a', 'b', 'c'})).to_equal {'a', 'b', 'c'} 235 | - it collects a table of key:value iterator results: 236 | t = {'first', second='two', last=3} 237 | expect(f(pairs, t)).to_equal(t) 238 | - it defaults to npairs iteration: 239 | expect(f {1, 2, [5]=5, a='b', c='d'}).to_equal {1, 2, [5]=5} 240 | - it propagates nil arguments correctly: 241 | expect(f {'a', nil, nil, 'd', 'e'}).to_equal {'a', [4]='d', [5]='e'} 242 | expect(f(iter, 'a', nil, nil, 'd', 'e')). 243 | to_equal {'a', false, false, 'd', 'e'} 244 | expect(f(iter, nil, nil, 3, 4, nil, nil)). 245 | to_equal {false, false, 3, 4, false, false} 246 | 247 | 248 | - describe compose: 249 | - before: 250 | f = M.compose 251 | 252 | - context with bad arguments: 253 | badargs.diagnose(f, 'std.functional.compose(func*)') 254 | 255 | - it composes a single function correctly: 256 | expect(f(M.id)(1)).to_be(1) 257 | - it propagates nil arguments correctly: 258 | expect(pack(f(M.id)(nil, 2, nil, nil))). 259 | to_equal(pack(nil, 2, nil, nil)) 260 | expect(pack(f(M.id, M.id)(nil, 2, nil, nil))). 261 | to_equal(pack(nil, 2, nil, nil)) 262 | - it composes functions in the correct order: 263 | expect(f(math.sin, math.cos)(1)). 264 | to_be(math.cos(math.sin(1))) 265 | 266 | 267 | - describe cond: 268 | - before: 269 | yes = function() return true end 270 | no = function() return false end 271 | default = function(s) return s end 272 | branches = {yes=yes, no=no, default} 273 | 274 | f = M.cond 275 | 276 | - it returns nil for no arguments: 277 | expect(f()).to_be(nil) 278 | - it evaluates a single function argument: 279 | expect(f(function() return true end)).to_be(true) 280 | - it evaluates a single functable argument: 281 | functable = setmetatable({}, {__call = function() return true end}) 282 | expect(f(functable)).to_be(true) 283 | - it returns a non-callable single argument directly: 284 | expect(f 'foo').to_be 'foo' 285 | - it evaluates a branch function if expr is truthy: 286 | expect(f('truthy', function(s) return s end)).to_be 'truthy' 287 | - it returns nil if the last expr is falsey: 288 | expect(f(nil, function(s) return 'falsey' end)).to_be(nil) 289 | expect(f(false, true, false, true)).to_be(nil) 290 | - it recurses with remaining arguments if first argument is falsey: 291 | expect(f(nil, true, 42, M.id)).to_be(42) 292 | expect(f(nil, true, false, false, 42, M.id)).to_be(42) 293 | 294 | 295 | - describe curry: 296 | - before: 297 | op = require 'std.functional.operator' 298 | 299 | f = M.curry 300 | 301 | - context with bad arguments: 302 | badargs.diagnose(f, 'std.functional.curry(func, int)') 303 | 304 | - it returns a zero argument function uncurried: 305 | expect(f(f, 0)).to_be(f) 306 | - it returns a one argument function uncurried: 307 | expect(f(f, 1)).to_be(f) 308 | - it curries a two argument function: 309 | expect(f(f, 2)).not_to_be(f) 310 | - it evaluates intermediate arguments one at a time: 311 | expect(f(math.min, 3)(2)(3)(4)).to_equal(2) 312 | - it returns a curried function that can be partially applied: 313 | bin = f(op.pow, 2)(2) 314 | expect(bin(2)).to_be(op.pow(2, 2)) 315 | expect(bin(10)).to_be(op.pow(2, 10)) 316 | 317 | 318 | - describe filter: 319 | - before: 320 | elements = {'a', 'b', 'c', 'd', 'e'} 321 | inverse = {a=1, b=2, c=3, d=4, e=5} 322 | 323 | f = M.filter 324 | 325 | - context with bad arguments: 326 | badargs.diagnose(f, 'std.functional.filter(func, [func], any*)') 327 | 328 | - it works with an empty table: 329 | expect(f(M.id, pairs, {})).to_equal {} 330 | - it iterates through element keys: 331 | expect(f(M.id, ielems, elements)).to_equal {'a', 'b', 'c', 'd', 'e'} 332 | expect(f(M.id, elems, inverse)).to_contain.a_permutation_of {1, 2, 3, 4, 5} 333 | - it propagates nil arguments correctly: 334 | t = {'a', nil, nil, 'd', 'e'} 335 | expect(f(M.id, npairs, t)).to_equal(t) 336 | expect(f(M.id, iter, nil, nil, 3, 4, nil, nil)). 337 | to_equal {false, false, 3, 4, false, false} 338 | - it passes all iteration result values to filter predicate: 339 | t = {} 340 | f(function(k, v) t[k] = v end, pairs, elements) 341 | expect(t).to_equal(elements) 342 | - it returns a list of filtered single return value iterator results: 343 | expect(f(function(e) return e:match '[aeiou]' end, ielems, elements)). 344 | to_equal {'a', 'e'} 345 | - it returns a table of filtered key:value iterator results: 346 | t = {'first', second=2, last='three'} 347 | expect(f(function(k, v) return type(v) == 'string' end, pairs, t)). 348 | to_equal {'first', last='three'} 349 | expect(f(function(k, v) return k % 2 == 0 end, ipairs, elements)). 350 | to_equal {[2]='b', [4]='d'} 351 | - it defaults to pairs iteration: 352 | t = {'first', second=2, last='three'} 353 | expect(f(function(k, v) return type(v) == 'string' end, t)). 354 | to_equal {'first', last='three'} 355 | 356 | 357 | - describe flatten: 358 | - before: 359 | t = {{{'one'}}, 'two', {{'three'}, 'four'}} 360 | 361 | f = M.flatten 362 | 363 | - context with bad arguments: 364 | badargs.diagnose(f, 'std.functional.flatten(table)') 365 | 366 | - it returns a table: 367 | expect(type(f(t))).to_be 'table' 368 | - it works for an empty table: 369 | expect(f {}).to_equal {} 370 | - it flattens a nested table: 371 | expect(f(t)).to_equal {'one', 'two', 'three', 'four'} 372 | 373 | 374 | - describe foldl: 375 | - before: 376 | op = require 'std.functional.operator' 377 | f = M.foldl 378 | 379 | - context with bad arguments: 380 | badargs.diagnose(f, 'std.functional.foldl(func, [any], table)') 381 | 382 | - it works with an empty table: 383 | expect(f(op.sum, 10000, {})).to_be(10000) 384 | - it folds a binary function through a table: 385 | expect(f(op.sum, 10000, {1, 10, 100})).to_be(10111) 386 | - it folds from left to right: 387 | expect(f(op.pow, 2, {3, 4})).to_be((2 ^ 3) ^ 4) 388 | - it supports eliding init argument: 389 | expect(f(op.pow, {2, 3, 4})).to_be((2 ^ 3) ^ 4) 390 | 391 | 392 | - describe foldr: 393 | - before: 394 | op = require 'std.functional.operator' 395 | f = M.foldr 396 | 397 | - context with bad arguments: 398 | badargs.diagnose(f, 'std.functional.foldr(func, [any], table)') 399 | 400 | - it works with an empty table: 401 | expect(f(op.sum, 1, {})).to_be(1) 402 | - it folds a binary function through a table: 403 | expect(f(op.sum, {10000, 100, 10, 1})).to_be(10111) 404 | - it folds from right to left: 405 | expect(f(op.quot, 10, {10000, 100})).to_be(10000 / (100 / 10)) 406 | - it supports eliding init argument: 407 | expect(f(op.quot, {10000, 100, 10})).to_be(10000 / (100 / 10)) 408 | 409 | 410 | - describe id: 411 | - before: 412 | f = M.id 413 | - it returns argument unchanged: 414 | expect(f(true)).to_be(true) 415 | expect(f {1, 1, 2, 3}).to_equal {1, 1, 2, 3} 416 | - it returns multiple arguments unchanged: 417 | expect({f(1, 'two', false)}).to_equal {1, 'two', false} 418 | - it propagates nil values correctly: 419 | expect(pack(f('a', nil, nil, 'd', 'e'))). 420 | to_equal(pack('a', nil, nil, 'd', 'e')) 421 | expect(pack(f(nil, nil, 3, 4, nil, nil))). 422 | to_equal(pack(nil, nil, 3, 4, nil, nil)) 423 | 424 | 425 | - describe ireverse: 426 | - before: | 427 | __index = setmetatable({ content = 'a string' }, { 428 | __index = function(t, n) 429 | if n <= #t.content then 430 | return t.content:sub(n, n) 431 | end 432 | end, 433 | __len = function(t) return #t.content end, 434 | }) 435 | 436 | f = M.ireverse 437 | 438 | - context with bad arguments: 439 | badargs.diagnose(f, 'std.functional.ireverse(table)') 440 | 441 | - it returns a new list: 442 | t = {1, 2, 5} 443 | expect(f(t)).not_to_be(t) 444 | - it reverses the elements relative to the original list: 445 | expect(f {1, 2, 'five'}).to_equal {'five', 2, 1} 446 | - it ignores the dictionary part of a table: 447 | expect(f {1, 2, 'five'; a = 'b', c = 'd'}).to_equal {'five', 2, 1} 448 | - it respects __len metamethod: 449 | expect(f(__index)).to_equal {'g', 'n', 'i', 'r', 't', 's', ' ', 'a'} 450 | - it works for an empty list: 451 | expect(f {}).to_equal {} 452 | 453 | 454 | - describe lambda: 455 | - before: 456 | f = M.lambda 457 | 458 | callable = function(x) 459 | if M.callable(x) then return true end 460 | return false 461 | end 462 | 463 | - context with bad arguments: 464 | badargs.diagnose(f, 'std.functional.lambda(string)') 465 | 466 | examples {['it diagnoses bad lambda string'] = function() 467 | expect(select(2, f 'foo')).to_be "invalid lambda string 'foo'" 468 | end} 469 | examples {['it diagnoses an uncompilable expression'] = function() 470 | expect(select(2, f '||+')).to_be "invalid lambda string '||+'" 471 | expect(select(2, f '=')).to_be "invalid lambda string '='" 472 | end} 473 | 474 | - it returns previously compiled lambdas: 475 | fn = f '|a,b|a==b' 476 | expect(fn).to_be(f '|a,b|a==b') 477 | 478 | - context with argument format: 479 | - it returns a callable: 480 | expect(callable(f '|x| 1+x')).to_be(true) 481 | - it compiles to a working Lua function: 482 | fn = f '||42' 483 | expect(fn()).to_be(42) 484 | - it propagates argument values: 485 | fn = f '|...| {...}' 486 | expect(fn(1,2,3)).to_equal {1,2,3} 487 | - it can be stringified: 488 | exp = '|a,b|a==b' 489 | expect(tostring(f(exp))).to_be(exp) 490 | - context with expression format: 491 | - it returns a function: 492 | expect(callable(f '_')).to_be(true) 493 | - it compiles to a working Lua function: 494 | fn = f '=42' 495 | expect(fn()).to_be(42) 496 | - it sets auto-argument values: 497 | fn = f '_*_' 498 | expect(fn(42)).to_be(1764) 499 | - it sets numeric auto-argument values: 500 | fn = f '_1+_2+_3' 501 | expect(fn(1, 2, 5)).to_be(8) 502 | - it can be stringified: 503 | exp = '_%2==0' 504 | expect(tostring(f(exp))).to_be(exp) 505 | 506 | 507 | - describe map: 508 | - before: 509 | elements = {'a', 'b', 'c', 'd', 'e'} 510 | inverse = {a=1, b=2, c=3, d=4, e=5} 511 | 512 | f = M.map 513 | 514 | - context with bad arguments: 515 | badargs.diagnose(f, 'std.functional.map(func, [func], ?any*)') 516 | 517 | - it works with an empty table: 518 | expect(f(M.id, ipairs, {})).to_equal {} 519 | - it iterates through elements: 520 | expect(f(M.id, ipairs, elements)).to_equal(elements) 521 | expect(f(M.id, pairs, inverse)).to_contain.a_permutation_of(elements) 522 | - it propagates nil arguments correctly: 523 | t = {'a', nil, nil, 'd', 'e'} 524 | expect(f(M.id, npairs, t)).to_equal(t) 525 | expect(f(M.id, iter, nil, nil, 3, 4, nil, nil)). 526 | to_equal {false, false, 3, 4, false, false} 527 | - it passes all iteration result values to map function: 528 | t = {} 529 | f(function(k, v) t[k] = v end, pairs, elements) 530 | expect(t).to_equal(elements) 531 | - it returns a list of mapped single return value iterator results: 532 | expect(f(function(e) return e:match '[aeiou]' end, ielems, elements)). 533 | to_equal {'a', 'e'} 534 | expect(f(function(e) return e .. 'x' end, elems, elements)). 535 | to_contain.a_permutation_of {'ax', 'bx', 'cx', 'dx', 'ex'} 536 | - it returns a table of mapped key:value iterator results: 537 | t = {'first', second=2, last='three'} 538 | expect(f(function(k, v) return type(v) == 'string' end, pairs, t)). 539 | to_contain.a_permutation_of {true, false, true} 540 | expect(f(function(k, v) return k % 2 == 0 end, ipairs, elements)). 541 | to_equal {false, true, false, true, false} 542 | - it supports key:value results from mapping function: 543 | expect(f(function(k, v) return v, k end, pairs, elements)). 544 | to_equal(inverse) 545 | - it defaults to pairs iteration: 546 | t = {'first', second=2, last='three'} 547 | expect(f(function(k, v) return type(v) == 'string' end, t)). 548 | to_contain.a_permutation_of {true, false, true} 549 | 550 | 551 | - describe map_with: 552 | - before: 553 | t = {{1, 2, 3}, {4, 5}} 554 | fn = function(...) return select('#', ...) end 555 | 556 | f = M.map_with 557 | 558 | - context with bad arguments: 559 | badargs.diagnose(f, 'std.functional.map_with(func, table of tables)') 560 | 561 | - it works for an empty table: 562 | expect(f(fn, {})).to_equal({}) 563 | - it returns a table: 564 | u = f(fn, t) 565 | expect(type(u)).to_be 'table' 566 | - it creates a new table: 567 | old = t 568 | u = f(fn, t) 569 | expect(t).to_equal(old) 570 | expect(u).not_to_equal(old) 571 | expect(t).to_equal {{1, 2, 3}, {4, 5}} 572 | - it maps a function over a list of argument lists: 573 | expect(f(fn, t)).to_equal {3, 2} 574 | - it discards hash-part arguments: 575 | expect(f(fn, {{1,x=2,3}, {4,5,y='z'}})).to_equal {2, 2} 576 | - it maps a function over a table of argument lists: 577 | expect(f(fn, {a={1,2,3}, b={4,5}})).to_equal {a=3, b=2} 578 | 579 | 580 | - describe memoize: 581 | - before: 582 | f = M.memoize 583 | 584 | memfn = f(function(...) 585 | n = select('#', ...) 586 | if n == 0 then return nil, 'bzzt' end 587 | return n, {...} 588 | end) 589 | 590 | - context with bad arguments: 591 | badargs.diagnose(f, 'std.functional.memoize(func, ?func)') 592 | 593 | - it propagates multiple return values: 594 | expect((memfn())).to_be(nil) 595 | expect(select(2, memfn())).to_be 'bzzt' 596 | - it propogates nil return values: 597 | expect(pack(f(function(...) return ... end)(nil, nil, 3, 4, nil, nil))). 598 | to_equal(pack(nil, nil, 3, 4, nil, nil)) 599 | - it propagates multiple arguments: 600 | expect({memfn('a', 42, false)}).to_equal {3, {'a', 42, false}} 601 | - it propagates nil arguments: 602 | n, t = memfn(nil) 603 | expect(t).to_equal({}) 604 | expect({memfn(nil, 2, nil, nil)}).to_equal {4, {[2]=2}} 605 | - it returns the same results for the same arguments: 606 | n, t = memfn(1) 607 | n, u = memfn(1) 608 | expect(t).to_be(u) 609 | - it returns a different object for different arguments: 610 | n, t = memfn(1) 611 | n, u = memfn(1, 2) 612 | expect(t).not_to_be(u) 613 | - it returns the same object for table valued arguments: 614 | n, t = memfn {1, 2, 3} 615 | n, u = memfn {1, 2, 3} 616 | expect(t).to_be(u) 617 | n, t = memfn {foo='bar', baz='quux'} 618 | n, u = memfn {foo='bar', baz='quux'} 619 | expect(t).to_be(u) 620 | n, t = memfn {baz='quux', foo='bar'} 621 | expect(t).to_be(u) 622 | - it returns a different object for different table arguments: 623 | n, t = memfn {1, 2, 3} 624 | n, u = memfn {1, 2} 625 | expect(t).not_to_be(u) 626 | n, u = memfn {3, 1, 2} 627 | expect(t).not_to_be(u) 628 | n, u = memfn {1, 2, 3, 4} 629 | expect(t).not_to_be(u) 630 | - it returns a different object for additional trailing nils: 631 | n, t = memfn(1, nil) 632 | n, u = memfn(1) 633 | expect(t).not_to_be(u) 634 | n, u = memfn(1, nil, nil) 635 | expect(t).not_to_be(u) 636 | - it accepts alternative mnemonic function: 637 | mnemonic = function(...) return select('#', ...) end 638 | memfn = f(function(x) return {x} end, mnemonic) 639 | expect(memfn 'same').to_be(memfn 'not same') 640 | expect(memfn(1, 2)).to_be(memfn(false, 'x')) 641 | expect(memfn 'one').not_to_be(memfn('one', 'two')) 642 | 643 | 644 | - describe nop: 645 | - before: 646 | f = M.nop 647 | - it accepts any number of arguments: 648 | expect(f()).to_be(nil) 649 | expect(f(false)).to_be(nil) 650 | expect(f(1, 2, 3, nil, 'str', {}, f)).to_be(nil) 651 | - it returns no values: 652 | expect(f(1, 'two', false)).to_be(nil) 653 | 654 | 655 | - describe product: 656 | - before: 657 | f = M.product 658 | 659 | - context with bad arguments: 660 | badargs.diagnose(f, 'std.functional.product(list*)') 661 | 662 | - it works with an empty table: 663 | expect(f {}).to_equal {} 664 | - it returns a list of elements from a single argument: 665 | expect(f {'a', 'b', 'c'}).to_equal {{'a'}, {'b'}, {'c'}} 666 | - it lists combinations with one element from each argument: 667 | expect(f({1, 2, 3}, {4, 5, 6})).to_equal { 668 | {1, 4}, {1, 5}, {1, 6}, 669 | {2, 4}, {2, 5}, {2, 6}, 670 | {3, 4}, {3, 5}, {3, 6}, 671 | } 672 | expect(f({1, 2}, {3, 4}, {5, 6})).to_equal { 673 | {1, 3, 5}, {1, 3, 6}, {1, 4, 5}, {1, 4, 6}, 674 | {2, 3, 5}, {2, 3, 6}, {2, 4, 5}, {2, 4, 6}, 675 | } 676 | expect(M.map(table.concat, ielems, f({0,1},{0,1},{0,1}))). 677 | to_equal {'000', '001', '010', '011', '100', '101', '110', '111'} 678 | 679 | 680 | - describe reduce: 681 | - before: 682 | op = require 'std.functional.operator' 683 | 684 | f = M.reduce 685 | 686 | - context with bad arguments: 687 | badargs.diagnose(f, 'std.functional.reduce(func, any, [func], ?any*)') 688 | 689 | - it works with an empty table: 690 | expect(f(op.sum, 2, ipairs, {})).to_be(2) 691 | - it calls a binary function over single return value iterator results: 692 | expect(f(op.sum, 2, ielems, {3})). 693 | to_be(2 + 3) 694 | expect(f(op.prod, 2, ielems, {3, 4})). 695 | to_be(2 * 3 * 4) 696 | - it calls a binary function over key:value iterator results: 697 | expect(f(op.sum, 2, ielems, {3})).to_be(2 + 3) 698 | expect(f(op.prod, 2, ielems, {3, 4})).to_be(2 * 3 * 4) 699 | - it propagates nil arguments correctly: 700 | function set(t, k, v) t[k] = tostring(v) return t end 701 | expect(f(set, {}, npairs, {1, nil, nil, 'a', false})). 702 | to_equal {'1', 'nil', 'nil', 'a', 'false'} 703 | expect(f(set, {}, npairs, {nil, nil, '3'})). 704 | to_equal {'nil', 'nil', '3'} 705 | expect(f(set, {}, iter, nil, nil, 3, 4, nil, nil)). 706 | to_equal {'false', 'false', '3', '4', 'false', 'false'} 707 | - it reduces elements from left to right: 708 | expect(f(op.pow, 2, ielems, {3, 4})).to_be((2 ^ 3) ^ 4) 709 | - it passes all iterator results to accumulator function: 710 | expect(f(rawset, {}, {'one', two=5})).to_equal {'one', two=5} 711 | 712 | 713 | - describe shape: 714 | - before: 715 | l = {1, 2, 3, 4, 5, 6} 716 | 717 | f = M.shape 718 | 719 | - context with bad arguments: 720 | badargs.diagnose(f, 'std.functional.shape(table, table)') 721 | 722 | - it returns a table: 723 | expect(objtype(f({2, 3}, l))).to_be 'table' 724 | - it works for an empty table: 725 | expect(f({0}, {})).to_equal({}) 726 | - it returns the result in a new table: 727 | expect(f({2, 3}, l)).not_to_be(l) 728 | - it does not perturb the argument table: 729 | f({2, 3}, l) 730 | expect(l).to_equal {1, 2, 3, 4, 5, 6} 731 | - it reshapes a table according to given dimensions: 732 | expect(f({2, 3}, l)). 733 | to_equal({{1, 2, 3}, {4, 5, 6}}) 734 | expect(f({3, 2}, l)). 735 | to_equal({{1, 2}, {3, 4}, {5, 6}}) 736 | - it treats 0-valued dimensions as an indefinite number: 737 | expect(f({2, 0}, l)). 738 | to_equal({{1, 2, 3}, {4, 5, 6}}) 739 | expect(f({0, 2}, l)). 740 | to_equal({{1, 2}, {3, 4}, {5, 6}}) 741 | 742 | 743 | - describe zip: 744 | - before: 745 | tt = {{1, 2}, {3, 4}, {5, 6}} 746 | 747 | f = M.zip 748 | 749 | - context with bad arguments: 750 | badargs.diagnose(f, 'std.functional.zip(table)') 751 | 752 | - it works for an empty table: 753 | expect(f {}).to_equal {} 754 | - it is the inverse of itself: 755 | expect(f(f(tt))).to_equal(tt) 756 | - it transposes rows and columns: 757 | expect(f(tt)).to_equal {{1, 3, 5}, {2, 4, 6}} 758 | expect(f {x={a=1, b=2}, y={a=3, b=4}, z={b=5}}). 759 | to_equal {a={x=1, y=3}, b={x=2,y=4,z=5}} 760 | 761 | 762 | - describe zip_with: 763 | - before: 764 | tt = {{1, 2}, {3, 4}, {5}} 765 | fn = function(...) return tonumber(table.concat {...}) end 766 | 767 | f = M.zip_with 768 | 769 | - context with bad arguments: 770 | badargs.diagnose(f, 'std.functional.zip_with(function, table of tables)') 771 | 772 | - it works for an empty table: 773 | expect(f(fn, {})).to_equal {} 774 | - it returns a table: 775 | expect(type(f(fn, tt))).to_be 'table' 776 | - it returns the result in a new table: 777 | expect(f(fn, tt)).not_to_be(tt) 778 | - it does not perturb the argument list: 779 | m = f(fn, tt) 780 | expect(tt).to_equal {{1, 2}, {3, 4}, {5}} 781 | - it combines column entries with a function: 782 | expect(f(fn, tt)).to_equal {135, 24} 783 | - it discards hash-part arguments: 784 | expect(f(fn, {{1,2}, x={3,4}, {[2]=5}})).to_equal {1, 25} 785 | - it combines matching key entries with a function: 786 | expect(f(fn, {{a=1,b=2}, {a=3,b=4}, {b=5}})). 787 | to_equal {a=13, b=245} 788 | -------------------------------------------------------------------------------- /spec/operator_spec.yaml: -------------------------------------------------------------------------------- 1 | # Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2022 std.functional authors 3 | 4 | before: | 5 | this_module = 'std.functional.operator' 6 | global_table = '_G' 7 | 8 | M = require(this_module) 9 | 10 | specify functional.operator: 11 | - context when required: 12 | - context by name: 13 | - it does not touch the global table: 14 | expect(show_apis {added_to=global_table, by=this_module}). 15 | to_equal {} 16 | 17 | 18 | - describe concat: 19 | - before: 20 | f = M.concat 21 | 22 | - it stringifies its arguments: 23 | expect(f(1, '')).to_be '1' 24 | expect(f('', 2)).to_be '2' 25 | - it concatenates its arguments: 26 | expect(f(1, 2)).to_be '12' 27 | 28 | 29 | - describe len: 30 | - before: 31 | f = M.len 32 | 33 | - context with string argument: 34 | - it returns the length of a string: 35 | expect(f '').to_be(0) 36 | expect(f 'abc').to_be(3) 37 | 38 | - context with table argument: 39 | - it returns the length of a table: 40 | expect(f {'a', 'b', 'c'}).to_be(3) 41 | expect(f {1, 2, 5, a=10, 3}).to_be(4) 42 | - it works with an empty table: 43 | expect(f {}).to_be(0) 44 | - it always ignores elements after the first hole: 45 | expect(f {1, 2, nil, nil, 3}).to_be(2) 46 | expect(f {1, 2, [5]=3}).to_be(2) 47 | - it respects __len metamethod: 48 | t = setmetatable({1, 2, [5]=3}, {__len = function() return 42 end}) 49 | expect(f(t)).to_be(42) 50 | 51 | - context with object argument: 52 | - before: 53 | mt = { 54 | _type = 'Object', 55 | __call = function(self, t) return setmetatable(t, getmetatable(self)) end, 56 | } 57 | Object = setmetatable({}, mt) 58 | subject = Object {'a', 'b', 'c'} 59 | 60 | - it returns the length of an object: 61 | expect(f(subject)).to_be(3) 62 | - it works with an empty object: 63 | expect(f(Object)).to_be(0) 64 | - it respects __len metamethod: 65 | mt.__len = function() return 42 end 66 | expect(f(Object {})).to_be(42) 67 | expect(f(subject {1, 2, 3})).to_be(42) 68 | 69 | 70 | - describe get: 71 | - before: 72 | f = M.get 73 | 74 | - it dereferences a table: 75 | expect(f({}, 1)).to_be(nil) 76 | expect(f({'foo', 'bar'}, 1)).to_be 'foo' 77 | expect(f({foo = 'bar'}, 'foo')).to_be 'bar' 78 | 79 | - describe set: 80 | - before: 81 | f = M.set 82 | 83 | - it sets a table entry: 84 | expect(f({}, 1, 42)).to_equal {42} 85 | expect(f({}, 'foo', 42)).to_equal {foo=42} 86 | - it overwrites an existing entry: 87 | expect(f({1, 2}, 1, 42)).to_equal {42, 2} 88 | expect(f({foo='bar', baz='quux'}, 'foo', 42)). 89 | to_equal {foo=42, baz='quux'} 90 | 91 | - describe sum: 92 | - before: 93 | f = M.sum 94 | 95 | - it returns the sum of its arguments: 96 | expect(f(99, 2)).to_be(99 + 2) 97 | 98 | - describe diff: 99 | - before: 100 | f = M.diff 101 | 102 | - it returns the difference of its arguments: 103 | expect(f(99, 2)).to_be(99 - 2) 104 | 105 | - describe prod: 106 | - before: 107 | f = M.prod 108 | 109 | - it returns the product of its arguments: 110 | expect(f(99, 2)).to_be(99 * 2) 111 | 112 | - describe quot: 113 | - before: 114 | f = M.quot 115 | 116 | - it returns the quotient of its arguments: 117 | expect(f(99, 2)).to_be(99 / 2) 118 | 119 | - describe mod: 120 | - before: 121 | f = M.mod 122 | 123 | - it returns the modulus of its arguments: 124 | expect(f(99, 2)).to_be(99 % 2) 125 | 126 | - describe pow: 127 | - before: 128 | f = M.pow 129 | 130 | - it returns the power of its arguments: 131 | expect(f(99, 2)).to_be(99 ^ 2) 132 | 133 | - describe conj: 134 | - before: 135 | f = M.conj 136 | 137 | - it returns the logical and of its arguments: 138 | expect(f(false, false)).to_be(false) 139 | expect(f(false, true)).to_be(false) 140 | expect(f(true, false)).to_be(false) 141 | expect(f(true, true)).to_be(true) 142 | - it supports truthy and falsey arguments: 143 | expect(f()).to_be(nil) 144 | expect(f(0)).to_be(nil) 145 | expect(f(nil, 0)).to_be(nil) 146 | expect(f(0, 'false')).to_be('false') 147 | 148 | - describe disj: 149 | - before: 150 | f = M.disj 151 | 152 | - it returns the logical or of its arguments: 153 | expect(f(false, false)).to_be(false) 154 | expect(f(false, true)).to_be(true) 155 | expect(f(true, false)).to_be(true) 156 | expect(f(true, true)).to_be(true) 157 | - it supports truthy and falsey arguments: 158 | expect(f()).to_be(nil) 159 | expect(f(0)).to_be(0) 160 | expect(f(nil, 0)).to_be(0) 161 | expect(f(0, 'false')).to_be(0) 162 | 163 | - describe neg: 164 | - before: 165 | f = M.neg 166 | 167 | - it returns the logical not of its argument: 168 | expect(f(false)).to_be(true) 169 | expect(f(true)).to_be(false) 170 | - it supports truthy and falsey arguments: 171 | expect(f()).to_be(true) 172 | expect(f(0)).to_be(false) 173 | 174 | - describe eqv: 175 | - before: | 176 | f = M.eqv 177 | 178 | __eq = function(a, b) return #a == #b end 179 | X = function(x) return setmetatable(x, {__eq = __eq}) end 180 | 181 | - it returns false if primitive types differ: 182 | expect(f(nil, 1)).to_be(false) 183 | expect(f('1', 1)).to_be(false) 184 | - it returns true if primitives are equal: 185 | expect(f(nil, nil)).to_be(true) 186 | expect(f(false, false)).to_be(true) 187 | expect(f(10, 10)).to_be(true) 188 | expect(f('one', 'one')).to_be(true) 189 | - it returns true if tables are equivalent: 190 | expect(f({}, {})).to_be(true) 191 | expect(f({'one'}, {'one'})).to_be(true) 192 | expect(f({1,2,3,4,5}, {1,2,3,4,5})).to_be(true) 193 | expect(f({a=1,b=2,c=3}, {c=3,b=2,a=1})).to_be(true) 194 | - it compares values recursively: 195 | expect(f({1, {{2, 3}, 4}}, {1, {{2, 3}, 4}})).to_be(true) 196 | expect(f({a=1, b={c={2, d=3}, 4}}, {a=1, b={c={2, d=3}, 4}})). 197 | to_be(true) 198 | - it compares keys recursively: 199 | expect(f({[{a=1}]=2}, {[{a=1}]=2})).to_be(true) 200 | expect(f({[{a=1}]={[{2}]='b'}}, {[{a=1}]={[{2}]='b'}})). 201 | to_be(true) 202 | - it returns false if table lengths differ: 203 | expect(f({1,2,3,4}, {1,2,3,4,5})).to_be(false) 204 | expect(f({1,2,3,4}, {[0]=0,1,2,3,4})).to_be(false) 205 | expect(f({[{a=1}]={[{2}]='b'}}, {[{a=1}]={[{2}]='b', 3}})). 206 | to_be(false) 207 | expect(f({[{a=1}]={[{2}]='b'}}, {[{a=1}]={[{2}]='b'}, 3})). 208 | to_be(false) 209 | - it returns true if __eq metamethod matches: 210 | expect(f(X {}, X {})).to_be(true) 211 | expect(f(X {1}, X {2})).to_be(true) 212 | - it returns false if _eq metamethod mismatches: 213 | expect(f(X {}, X {1})).to_be(false) 214 | 215 | - describe eq: 216 | - before: 217 | f = M.eq 218 | 219 | - it returns true if the arguments are equal: 220 | expect(f()).to_be(true) 221 | expect(f('foo', 'foo')).to_be(true) 222 | - it returns false if the arguments are unequal: 223 | expect(f(1)).to_be(false) 224 | expect(f('foo', 'bar')).to_be(false) 225 | 226 | - describe neq: 227 | - before: 228 | f = M.neq 229 | 230 | - it returns false if the arguments are equal: 231 | expect(f(1, 1)).to_be(false) 232 | expect(f('foo', 'foo')).to_be(false) 233 | - it returns true if the arguments are unequal: 234 | expect(f(1)).to_be(true) 235 | expect(f('foo', 'bar')).to_be(true) 236 | expect(f({}, {})).to_be(true) 237 | 238 | - describe lt: 239 | - before: 240 | f = M.lt 241 | 242 | - it returns true if the arguments are in ascending order: 243 | expect(f(1, 2)).to_be(true) 244 | expect(f('a', 'b')).to_be(true) 245 | - it returns false if the arguments are not in ascending order: 246 | expect(f(2, 2)).to_be(false) 247 | expect(f(3, 2)).to_be(false) 248 | expect(f('b', 'b')).to_be(false) 249 | expect(f('c', 'b')).to_be(false) 250 | - it supports __lt metamethods: 251 | List = require 'std.list' {} 252 | expect(f(List {1, 2, 3}, List {1, 2, 3, 4})).to_be(true) 253 | expect(f(List {1, 2, 3}, List {1, 2, 3})).to_be(false) 254 | expect(f(List {1, 2, 4}, List {1, 2, 3})).to_be(false) 255 | 256 | - describe lte: 257 | - before: 258 | f = M.lte 259 | 260 | - it returns true if the arguments are not in descending order: 261 | expect(f(1, 2)).to_be(true) 262 | expect(f(2, 2)).to_be(true) 263 | expect(f('a', 'b')).to_be(true) 264 | expect(f('b', 'b')).to_be(true) 265 | - it returns false if the arguments are in descending order: 266 | expect(f(3, 2)).to_be(false) 267 | expect(f('c', 'b')).to_be(false) 268 | - it supports __lte metamethods: 269 | List = require 'std.list' {} 270 | expect(f(List {1, 2, 3}, List {1, 2, 3, 4})).to_be(true) 271 | expect(f(List {1, 2, 3}, List {1, 2, 3})).to_be(true) 272 | expect(f(List {1, 2, 4}, List {1, 2, 3})).to_be(false) 273 | 274 | - describe gt: 275 | - before: 276 | f = M.gt 277 | 278 | - it returns true if the arguments are in descending order: 279 | expect(f(2, 1)).to_be(true) 280 | expect(f('b', 'a')).to_be(true) 281 | - it returns false if the arguments are not in descending order: 282 | expect(f(2, 2)).to_be(false) 283 | expect(f(2, 3)).to_be(false) 284 | expect(f('b', 'b')).to_be(false) 285 | expect(f('b', 'c')).to_be(false) 286 | - it supports __lt metamethods: 287 | List = require 'std.list' {} 288 | expect(f(List {1, 2, 3, 4}, List {1, 2, 3})).to_be(true) 289 | expect(f(List {1, 2, 3}, List {1, 2, 3})).to_be(false) 290 | expect(f(List {1, 2, 3}, List {1, 2, 4})).to_be(false) 291 | 292 | - describe gte: 293 | - before: 294 | f = M.gte 295 | 296 | - it returns true if the arguments are not in ascending order: 297 | expect(f(2, 1)).to_be(true) 298 | expect(f(2, 2)).to_be(true) 299 | expect(f('b', 'a')).to_be(true) 300 | expect(f('b', 'b')).to_be(true) 301 | - it returns false if the arguments are in ascending order: 302 | expect(f(2, 3)).to_be(false) 303 | expect(f('b', 'c')).to_be(false) 304 | - it supports __lte metamethods: 305 | List = require 'std.list' {} 306 | expect(f(List {1, 2, 3, 4}, List {1, 2, 3})).to_be(true) 307 | expect(f(List {1, 2, 3}, List {1, 2, 3})).to_be(true) 308 | expect(f(List {1, 2, 3}, List {1, 2, 4})).to_be(false) 309 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 3 | Copyright (C) 2014-2022 std.functional authors 4 | ]] 5 | local typecheck 6 | have_typecheck, typecheck = pcall(require, 'typecheck') 7 | 8 | local inprocess = require 'specl.inprocess' 9 | local hell = require 'specl.shell' 10 | local std = require 'specl.std' 11 | 12 | badargs = require 'specl.badargs' 13 | 14 | package.path = std.package.normalize('./lib/?.lua', './lib/?/init.lua', package.path) 15 | 16 | 17 | -- Allow user override of LUA binary used by hell.spawn, falling 18 | -- back to environment PATH search for 'lua' if nothing else works. 19 | local LUA = os.getenv 'LUA' or 'lua' 20 | 21 | 22 | -- Allow use of bare 'unpack' even in Lua 5.3. 23 | unpack = table.unpack or unpack 24 | 25 | 26 | pack = table.pack or function(...) 27 | return {n = select('#', ...), ...} 28 | end 29 | 30 | 31 | -- In case we're not using a bleeding edge release of Specl... 32 | _diagnose = badargs.diagnose 33 | badargs.diagnose = function(...) 34 | if have_typecheck then 35 | return _diagnose(...) 36 | end 37 | end 38 | 39 | 40 | -- A copy of base.lua:type, so that an unloadable base.lua doesn't 41 | -- prevent everything else from working. 42 | function objtype(o) 43 | return(getmetatable(o) or {})._type or io.type(o) or type(o) 44 | end 45 | 46 | 47 | local function mkscript(code) 48 | local f = os.tmpname() 49 | local h = io.open(f, 'w') 50 | h:write(code) 51 | h:close() 52 | return f 53 | end 54 | 55 | 56 | --- Run some Lua code with the given arguments and input. 57 | -- @string code valid Lua code 58 | -- @tparam[opt={}] string|table arg single argument, or table of 59 | -- arguments for the script invocation. 60 | -- @string[opt] stdin standard input contents for the script process 61 | -- @treturn specl.shell.Process|nil status of resulting process if 62 | -- execution was successful, otherwise nil 63 | function luaproc(code, arg, stdin) 64 | local f = mkscript(code) 65 | if type(arg) ~= 'table' then arg = {arg} end 66 | local cmd = {LUA, f, unpack(arg)} 67 | -- inject env and stdin keys separately to avoid truncating `...` in 68 | -- cmd constructor 69 | cmd.env = {LUA_PATH=package.path, LUA_INIT='', LUA_INIT_5_2=''} 70 | cmd.stdin = stdin 71 | local proc = hell.spawn(cmd) 72 | os.remove(f) 73 | return proc 74 | end 75 | 76 | 77 | local function tabulate_output(code) 78 | local proc = luaproc(code) 79 | if proc.status ~= 0 then return error(proc.errout) end 80 | local r = {} 81 | proc.output:gsub('(%S*)[%s]*', 82 | function(x) 83 | if x ~= '' then r[x] = true end 84 | end) 85 | return r 86 | end 87 | 88 | 89 | --- Show changes to tables wrought by a require statement. 90 | -- There are a few modes to this function, controlled by what named 91 | -- arguments are given. Lists new keys in T1 after `require "import"`: 92 | -- 93 | -- show_apis {added_to=T1, by=import} 94 | -- 95 | -- @tparam table argt one of the combinations above 96 | -- @treturn table a list of keys according to criteria above 97 | function show_apis(argt) 98 | return tabulate_output([[ 99 | local before, after = {}, {} 100 | for k in pairs(]] .. argt.added_to .. [[) do 101 | before[k] = true 102 | end 103 | 104 | local M = require ']] .. argt.by .. [[' 105 | for k in pairs(]] .. argt.added_to .. [[) do 106 | after[k] = true 107 | end 108 | 109 | for k in pairs(after) do 110 | if not before[k] then print(k) end 111 | end 112 | ]]) 113 | end 114 | -------------------------------------------------------------------------------- /spec/tuple_spec.yaml: -------------------------------------------------------------------------------- 1 | # Functional programming for Lua 5.1, 5.2, 5.3 & 5.4 2 | # Copyright (C) 2014-2022 std.functional authors 3 | 4 | before: 5 | Tuple = require 'std.functional.tuple' 6 | 7 | t0, t1, t2 = Tuple(), Tuple 'one', Tuple(false, true) 8 | 9 | 10 | specify functional.tuple: 11 | - describe require: 12 | - it does not perturb the global namespace: 13 | expect(show_apis {added_to='_G', by='std.functional.tuple'}). 14 | to_equal {} 15 | 16 | 17 | - describe construction: 18 | - it constructs a new tuple: 19 | expect(t0).not_to_be(Tuple) 20 | expect(objtype(t0)).to_be 'Tuple' 21 | - it initialises tuple with constructor parameters: 22 | expect(objtype(t2)).to_be 'Tuple' 23 | expect(t2[1]).to_be(false) 24 | expect(t2[2]).to_be(true) 25 | - it understands nil valued elements: 26 | t5 = Tuple(nil, nil, 1, nil, nil) 27 | expect(t5[3]).to_be(1) 28 | expect(t5[5]).to_be(nil) 29 | 30 | 31 | - describe length: 32 | - context with n field: 33 | - it returns the number of elements: 34 | expect(t0.n).to_be(0) 35 | expect(t1.n).to_be(1) 36 | expect(t2.n).to_be(2) 37 | - it counts nil valued elements: 38 | expect(Tuple(nil).n).to_be(1) 39 | expect(Tuple(nil, false, nil, nil).n).to_be(4) 40 | 41 | - context with functional.operator.len: 42 | - before: 43 | len = require 'std.functional.operator'.len 44 | - it returns the number of elements: 45 | expect(len(t0)).to_be(0) 46 | expect(len(t1)).to_be(1) 47 | expect(len(t2)).to_be(2) 48 | - it counts nil valued elements: 49 | expect(len(Tuple(nil))).to_be(1) 50 | expect(len(Tuple(nil, false, nil, nil))).to_be(4) 51 | 52 | 53 | - describe indexing: 54 | - it dereferences elements: 55 | expect(t2[1]).to_be(false) 56 | expect(t2[2]).to_be(true) 57 | expect(t2.n).to_be(2) 58 | - it returns nil-valued elements: 59 | t3 = Tuple(nil, false, nil) 60 | expect(t3[1]).to_be(nil) 61 | expect(t3[2]).to_be(false) 62 | expect(t3[3]).to_be(nil) 63 | expect(t3.n).to_be(3) 64 | - it returns nil for out-of-bound indices: 65 | expect(t1[0]).to_be(nil) 66 | expect(t1[1]).not_to_be(nil) 67 | expect(t1[2]).to_be(nil) 68 | expect(t1[-1]).to_be(nil) 69 | expect(t1.foo).to_be(nil) 70 | 71 | 72 | - describe interning: 73 | - it interns all tuples: 74 | expect(Tuple()).to_be(Tuple()) 75 | expect(Tuple('a', 2)).to_be(Tuple('a', 2)) 76 | - it interns nil valued elements: 77 | expect(Tuple(nil)).to_be(Tuple(nil)) 78 | expect(Tuple(nil, false, nil)).to_be(Tuple(nil, false, nil)) 79 | expect(Tuple(nil)).not_to_be(Tuple(false)) 80 | - it distinguishes nil from no elements: 81 | expect(Tuple(nil)).not_to_be(Tuple()) 82 | 83 | 84 | - describe immutability: 85 | - before: 86 | fn = function(t, k, v) t[k] = v end 87 | 88 | - it diagnoses attempts to mutate contents: 89 | expect(fn(t2, 1, 1)).to_error 'cannot change immutable tuple object' 90 | - it diagnoses attempts to mutate methods: 91 | expect(fn(t2, 'unpack', nil)).to_error 'cannot change immutable tuple object' 92 | - it diagnoses attempts to add new values: 93 | expect(fn(t2, 0, 'oob')).to_error 'cannot change immutable tuple object' 94 | 95 | 96 | - describe traversing: 97 | - before: 98 | pretty = function(t) 99 | local r = {} 100 | for i = 1, t.n do r[i] = tostring(t[i]) end 101 | return table.concat(r, ',') 102 | end 103 | 104 | - it iterates over the elements: 105 | expect(pretty(Tuple('a', 'b', 'c'))).to_be 'a,b,c' 106 | - it works with 0-tuple: 107 | expect(pretty(Tuple())).to_be '' 108 | - it understands nil elements: 109 | expect(pretty(Tuple(nil))).to_be 'nil' 110 | expect(pretty(Tuple(false, nil))).to_be 'false,nil' 111 | expect(pretty(Tuple(nil, false))).to_be 'nil,false' 112 | expect(pretty(Tuple(nil, nil))).to_be 'nil,nil' 113 | 114 | 115 | - describe unpacking: 116 | - before: 117 | collect = function(t) 118 | local r = {t()} 119 | for i = 1, t.n do r[i] = tostring(r[i]) end 120 | return table.concat(r, ',') 121 | end 122 | 123 | - it returns all elements: 124 | expect(collect(Tuple('a', 'b', 'unpack'))).to_be 'a,b,unpack' 125 | - it works with 0-tuple: 126 | expect(collect(Tuple())).to_be '' 127 | - it understands nil elements: 128 | expect(collect(Tuple(nil))).to_be 'nil' 129 | expect(collect(Tuple(false, nil))).to_be 'false,nil' 130 | expect(collect(Tuple(nil, false))).to_be 'nil,false' 131 | expect(collect(Tuple(nil, nil))).to_be 'nil,nil' 132 | 133 | 134 | - describe __tostring: 135 | - it starts with the object type: 136 | expect(tostring(Tuple())).to_match '^Tuple' 137 | - it contains all the elements: 138 | elems = {'a', 'b', 'c'} 139 | for _, e in ipairs(elems) do 140 | expect(tostring(Tuple(unpack(elems)))).to_match(e) 141 | end 142 | -------------------------------------------------------------------------------- /std.functional-git-1.rockspec: -------------------------------------------------------------------------------- 1 | local _MODREV, _SPECREV = 'git', '-1' 2 | 3 | package = 'std.functional' 4 | version = _MODREV .. _SPECREV 5 | 6 | description = { 7 | summary = 'Functional Programming with Lua', 8 | detailed = [[ 9 | An immutable tuple object, and many higher-order functions to help 10 | program in a functional style using tuples and Lua tables. 11 | ]], 12 | homepage = 'http://lua-stdlib.github.io/functional', 13 | license = 'MIT/X11', 14 | } 15 | 16 | dependencies = { 17 | 'lua >= 5.1, < 5.5', 18 | 'std.normalize >= 2.0.3', 19 | } 20 | 21 | source = { 22 | url = 'http://github.com/lua-stdlib/functional/archive/v' .. _MODREV .. '.zip', 23 | dir = 'functional-' .. _MODREV, 24 | } 25 | 26 | build = { 27 | type = 'builtin', 28 | modules = { 29 | ['std.functional'] = 'lib/std/functional/init.lua', 30 | ['std.functional._base'] = 'lib/std/functional/_base.lua', 31 | ['std.functional.operator'] = 'lib/std/functional/operator.lua', 32 | ['std.functional.tuple'] = 'lib/std/functional/tuple.lua', 33 | ['std.functional.version'] = 'lib/std/functional/version.lua', 34 | }, 35 | copy_directories = {'doc'}, 36 | } 37 | 38 | if _MODREV == 'git' then 39 | build.copy_directories = nil 40 | 41 | source = { 42 | url = 'git://github.com/lua-stdlib/functional.git', 43 | } 44 | end 45 | --------------------------------------------------------------------------------