• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 4843628693

pending completion
4843628693

push

github

GitHub
chore(ci): Bump luacheck workflow version (#453)

5279 of 6022 relevant lines covered (87.66%)

45.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.86
/lua/pl/func.lua
1
--- Functional helpers like composition, binding and placeholder expressions.
2
-- Placeholder expressions are useful for short anonymous functions, and were
3
-- inspired by the Boost Lambda library.
4
--
5
--    > utils.import 'pl.func'
6
--    > ls = List{10,20,30}
7
--    > = ls:map(_1+1)
8
--    {11,21,31}
9
--
10
-- They can also be used to _bind_ particular arguments of a function.
11
--
12
--    > p = bind(print,'start>',_0)
13
--    > p(10,20,30)
14
--    > start>   10   20  30
15
--
16
-- See @{07-functional.md.Creating_Functions_from_Functions|the Guide}
17
--
18
-- Dependencies: `pl.utils`, `pl.tablex`
19
-- @module pl.func
20
local type,setmetatable,getmetatable,rawset = type,setmetatable,getmetatable,rawset
3✔
21
local concat,append = table.concat,table.insert
3✔
22
local tostring = tostring
3✔
23
local utils = require 'pl.utils'
3✔
24
local pairs,rawget,unpack,pack = pairs,rawget,utils.unpack,utils.pack
3✔
25
local tablex = require 'pl.tablex'
3✔
26
local map = tablex.map
3✔
27
local _DEBUG = rawget(_G,'_DEBUG')
3✔
28
local assert_arg = utils.assert_arg
3✔
29

30
local func = {}
3✔
31

32
-- metatable for Placeholder Expressions (PE)
33
local _PEMT = {}
3✔
34

35
local function P (t)
36
    setmetatable(t,_PEMT)
145✔
37
    return t
145✔
38
end
39

40
func.PE = P
3✔
41

42
local function isPE (obj)
43
    return getmetatable(obj) == _PEMT
878✔
44
end
45

46
func.isPE = isPE
3✔
47

48
-- construct a placeholder variable (e.g _1 and _2)
49
local function PH (idx)
50
    return P {op='X',repr='_'..idx, index=idx}
15✔
51
end
52

53
-- construct a constant placeholder variable (e.g _C1 and _C2)
54
local function CPH (idx)
55
    return P {op='X',repr='_C'..idx, index=idx}
14✔
56
end
57

58
func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
18✔
59
func._0 = P{op='X',repr='...',index=0}
6✔
60

61
function func.Var (name)
3✔
62
    local ls = utils.split(name,'[%s,]+')
3✔
63
    local res = {}
3✔
64
    for i = 1, #ls do
6✔
65
        append(res,P{op='X',repr=ls[i],index=0})
6✔
66
    end
67
    return unpack(res)
3✔
68
end
69

70
function func._ (value)
3✔
71
    return P{op='X',repr=value,index='wrap'}
×
72
end
73

74
local repr
75

76
func.Nil = func.Var 'nil'
6✔
77

78
function _PEMT.__index(obj,key)
3✔
79
    return P{op='[]',obj,key}
4✔
80
end
81

82
function _PEMT.__call(fun,...)
3✔
83
    return P{op='()',fun,...}
52✔
84
end
85

86
function _PEMT.__tostring (e)
3✔
87
    return repr(e)
×
88
end
89

90
function _PEMT.__unm(arg)
3✔
91
    return P{op='unm',arg}
8✔
92
end
93

94
function func.Not (arg)
3✔
95
    return P{op='not',arg}
2✔
96
end
97

98
function func.Len (arg)
3✔
99
    return P{op='#',arg}
1✔
100
end
101

102

103
local function binreg(context,t)
104
    for name,op in pairs(t) do
54✔
105
        rawset(context,name,function(x,y)
90✔
106
            return P{op=op,x,y}
43✔
107
        end)
108
    end
109
end
110

111
local function import_name (name,fun,context)
112
    rawset(context,name,function(...)
×
113
        return P{op='()',fun,...}
×
114
    end)
115
end
116

117
local imported_functions = {}
3✔
118

119
local function is_global_table (n)
120
    return type(_G[n]) == 'table'
×
121
end
122

123
--- wrap a table of functions. This makes them available for use in
124
-- placeholder expressions.
125
-- @string tname a table name
126
-- @tab context context to put results, defaults to environment of caller
127
function func.import(tname,context)
3✔
128
    assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
×
129
    local t = _G[tname]
×
130
    context = context or _G
×
131
    for name,fun in pairs(t) do
×
132
        import_name(name,fun,context)
×
133
        imported_functions[fun] = name
×
134
    end
135
end
136

137
--- register a function for use in placeholder expressions.
138
-- @func fun a function
139
-- @string[opt] name an optional name
140
-- @return a placeholder functiond
141
function func.register (fun,name)
3✔
142
    assert_arg(1,fun,'function')
×
143
    if name then
×
144
        assert_arg(2,name,'string')
×
145
        imported_functions[fun] = name
×
146
    end
147
    return function(...)
148
        return P{op='()',fun,...}
×
149
    end
150
end
151

152
function func.lookup_imported_name (fun)
3✔
153
    return imported_functions[fun]
×
154
end
155

156
local function _arg(...) return ... end
3✔
157

158
function func.Args (...)
3✔
159
    return P{op='()',_arg,...}
×
160
end
161

162
-- binary operators with their precedences (see Lua manual)
163
-- precedences might be incremented by one before use depending on
164
-- left- or right-associativity, space them out
165
local binary_operators = {
3✔
166
    ['or'] = 0,
167
    ['and'] = 2,
168
    ['=='] = 4, ['~='] = 4, ['<'] = 4, ['>'] = 4,  ['<='] = 4,   ['>='] = 4,
169
    ['..'] = 6,
170
    ['+'] = 8, ['-'] = 8,
171
    ['*'] = 10, ['/'] = 10, ['%'] = 10,
172
    ['^'] = 14
×
173
}
174

175
-- unary operators with their precedences
176
local unary_operators = {
3✔
177
    ['not'] = 12, ['#'] = 12, ['unm'] = 12
×
178
}
179

180
-- comparisons (as prefix functions)
181
binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
3✔
182

183
-- standard binary operators (as metamethods)
184
binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
3✔
185

186
binreg (_PEMT,{__eq='=='})
3✔
187

188
--- all elements of a table except the first.
189
-- @tab ls a list-like table.
190
function func.tail (ls)
3✔
191
    assert_arg(1,ls,'table')
52✔
192
    local res = {}
52✔
193
    for i = 2,#ls do
60✔
194
        append(res,ls[i])
8✔
195
    end
196
    return res
52✔
197
end
198

199
--- create a string representation of a placeholder expression.
200
-- @param e a placeholder expression
201
-- @param lastpred not used
202
function repr (e,lastpred)
3✔
203
    local tail = func.tail
526✔
204
    if isPE(e) then
1,052✔
205
        local pred = binary_operators[e.op] or unary_operators[e.op]
200✔
206
        if pred then
200✔
207
            -- binary or unary operator
208
            local s
209
            if binary_operators[e.op] then
54✔
210
                local left_pred = pred
43✔
211
                local right_pred = pred
43✔
212
                if e.op == '..' or e.op == '^' then
43✔
213
                    left_pred = left_pred + 1
16✔
214
                else
215
                    right_pred = right_pred + 1
27✔
216
                end
217
                local left_arg = repr(e[1], left_pred)
43✔
218
                local right_arg = repr(e[2], right_pred)
43✔
219
                s = left_arg..' '..e.op..' '..right_arg
43✔
220
            else
221
                local op = e.op == 'unm' and '-' or e.op
11✔
222
                s = op..' '..repr(e[1], pred)
22✔
223
            end
224
            if lastpred and lastpred > pred then
54✔
225
                s = '('..s..')'
6✔
226
            end
227
            return s
54✔
228
        else -- either postfix, or a placeholder
229
            local ls = map(repr,e)
146✔
230
            if e.op == '[]' then
146✔
231
                return ls[1]..'['..ls[2]..']'
4✔
232
            elseif e.op == '()' then
142✔
233
                local fn
234
                if ls[1] ~= nil then -- was _args, undeclared!
52✔
235
                    fn = ls[1]
52✔
236
                else
237
                    fn = ''
×
238
                end
239
                return fn..'('..concat(tail(ls),',')..')'
104✔
240
            else
241
                return e.repr
90✔
242
            end
243
        end
244
    elseif type(e) == 'string' then
326✔
245
        return '"'..e..'"'
236✔
246
    elseif type(e) == 'function' then
90✔
247
        local name = func.lookup_imported_name(e)
×
248
        if name then return name else return tostring(e) end
×
249
    else
250
        return tostring(e) --should not really get here!
90✔
251
    end
252
end
253
func.repr = repr
3✔
254

255
-- collect all the non-PE values in this PE into vlist, and replace each occurence
256
-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
257
local collect_values
258
function collect_values (e,vlist)
3✔
259
    if isPE(e) then
366✔
260
        if e.op ~= 'X' then
183✔
261
            local m = 0
109✔
262
            for i = 1,#e do
272✔
263
                local subx = e[i]
163✔
264
                local pe = isPE(subx)
163✔
265
                if pe then
163✔
266
                    if subx.op == 'X' and subx.index == 'wrap' then
149✔
267
                        subx = subx.repr
×
268
                        pe = false
×
269
                    else
270
                        m = math.max(m,collect_values(subx,vlist))
298✔
271
                    end
272
                end
273
                if not pe then
163✔
274
                    append(vlist,subx)
14✔
275
                    e[i] = CPH(#vlist)
28✔
276
                end
277
            end
278
            return m
109✔
279
        else -- was a placeholder, it has an index...
280
            return e.index
74✔
281
        end
282
    else -- plain value has no placeholder dependence
283
        return 0
×
284
    end
285
end
286
func.collect_values = collect_values
3✔
287

288
--- instantiate a PE into an actual function. First we find the largest placeholder used,
289
-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
290
-- any non-PE values from the PE, and build up a constant binding list.
291
-- Finally, the expression can be compiled, and e.__PE_function is set.
292
-- @param e a placeholder expression
293
-- @return a function
294
function func.instantiate (e)
3✔
295
    local consts,values,parms = {},{},{}
13✔
296
    local rep, err, fun
297
    local n = func.collect_values(e,values)
13✔
298
    for i = 1,#values do
25✔
299
        append(consts,'_C'..i)
12✔
300
        if _DEBUG then print(i,values[i]) end
12✔
301
    end
302
    for i =1,n do
32✔
303
        append(parms,'_'..i)
19✔
304
    end
305
    consts = concat(consts,',')
13✔
306
    parms = concat(parms,',')
13✔
307
    rep = repr(e)
26✔
308
    local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
13✔
309
    if _DEBUG then print(fstr) end
13✔
310
    fun,err = utils.load(fstr,'fun')
13✔
311
    if not fun then return nil,err end
13✔
312
    fun = fun()  -- get wrapper
26✔
313
    fun = fun(unpack(values)) -- call wrapper (values could be empty)
39✔
314
    e.__PE_function = fun
13✔
315
    return fun
13✔
316
end
317

318
--- instantiate a PE unless it has already been done.
319
-- @param e a placeholder expression
320
-- @return the function
321
function func.I(e)
3✔
322
    if rawget(e,'__PE_function')  then
9✔
323
        return e.__PE_function
×
324
    else return func.instantiate(e)
9✔
325
    end
326
end
327

328
utils.add_function_factory(_PEMT,func.I)
3✔
329

330
--- bind the first parameter of the function to a value.
331
-- @function func.bind1
332
-- @func fn a function of one or more arguments
333
-- @param p a value
334
-- @return a function of one less argument
335
-- @usage (bind1(math.max,10))(20) == math.max(10,20)
336
func.bind1 = utils.bind1
3✔
337
func.curry = func.bind1
3✔
338

339
--- create a function which chains multiple functions.
340
-- @func f a function of at least one argument
341
-- @func g a function of at least one argument
342
-- @param ... additional functions to compose
343
-- @return a function
344
-- @usage printf = compose(io.write, string.format)
345
-- @usage printf = compose(io.write, string.lower, string.format)
346
function func.compose (...)
3✔
347
    local args = pack(...)
6✔
348
    return tablex.reduce(function(f, g)
6✔
349
      return function(...)
350
        return f(g(...))
12✔
351
      end
352
    end, args)
6✔
353
end
354

355
--- bind the arguments of a function to given values.
356
-- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`.
357
-- @func fn a function of at least one argument
358
-- @param ... values or placeholder variables
359
-- @return a function
360
-- @usage (bind(f,_1,a))(b) == f(a,b)
361
-- @usage (bind(f,_2,_1))(a,b) == f(b,a)
362
function func.bind(fn,...)
3✔
363
    local args = pack(...)
2✔
364
    local holders,parms,bvalues,values = {},{},{'fn'},{}
2✔
365
    local nv,maxplace,varargs = 1,0,false
2✔
366
    for i = 1,args.n do
8✔
367
        local a = args[i]
6✔
368
        if isPE(a) and a.op == 'X' then
12✔
369
            append(holders,a.repr)
4✔
370
            maxplace = math.max(maxplace,a.index)
4✔
371
            if a.index == 0 then varargs = true end
4✔
372
        else
373
            local v = '_v'..nv
2✔
374
            append(bvalues,v)
2✔
375
            append(holders,v)
2✔
376
            append(values,a)
2✔
377
            nv = nv + 1
2✔
378
        end
379
    end
380
    for np = 1,maxplace do
6✔
381
        append(parms,'_'..np)
4✔
382
    end
383
    if varargs then append(parms,'...') end
2✔
384
    bvalues = concat(bvalues,',')
2✔
385
    parms = concat(parms,',')
2✔
386
    holders = concat(holders,',')
2✔
387
    local fstr = ([[
388
return function (%s)
389
    return function(%s) return fn(%s) end
390
end
391
]]):format(bvalues,parms,holders)
2✔
392
    if _DEBUG then print(fstr) end
2✔
393
    local res = utils.load(fstr)
2✔
394
    res = res()
4✔
395
    return res(fn,unpack(values))
4✔
396
end
397

398
return func
3✔
399

400

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc